Fixes #4112. WordForward and WordBackward are not consistent with identical RuneType (#4131)

* Move parallelizable to new file

* Add UseSameRuneTypeForWords property

* Add SelectWordOnlyOnDoubleClick property and ProcessDoubleClickSelection method

* Change IsSameRuneType method to also handle equivalent rune types

* Fix WordBackward and WordForward to support properly handle rune types

* Fix unit test to deal properly with the new roles of rune types

* Add new unit tests

* Remove duplicated unit test

* Add UseSameRuneTypeForWords and SelectWordOnlyOnDoubleClick handling into Editor scenario
This commit is contained in:
BDisp
2025-06-09 21:29:45 +01:00
committed by GitHub
parent b59ac3b69c
commit e1086a45a9
8 changed files with 2777 additions and 2239 deletions

View File

@@ -201,6 +201,8 @@ public class Editor : Scenario
CreateAutocomplete (), CreateAutocomplete (),
CreateAllowsTabChecked (), CreateAllowsTabChecked (),
CreateReadOnlyChecked (), CreateReadOnlyChecked (),
CreateUseSameRuneTypeForWords (),
CreateSelectWordOnlyOnDoubleClick (),
new MenuItem ( new MenuItem (
"Colors", "Colors",
"", "",
@@ -776,6 +778,26 @@ public class Editor : Scenario
return new [] { item }; return new [] { item };
} }
private MenuItem CreateSelectWordOnlyOnDoubleClick ()
{
var item = new MenuItem { Title = "SelectWordOnlyOnDoubleClick" };
item.CheckType |= MenuItemCheckStyle.Checked;
item.Checked = _textView.SelectWordOnlyOnDoubleClick;
item.Action += () => _textView.SelectWordOnlyOnDoubleClick = (bool)(item.Checked = !item.Checked);
return item;
}
private MenuItem CreateUseSameRuneTypeForWords ()
{
var item = new MenuItem { Title = "UseSameRuneTypeForWords" };
item.CheckType |= MenuItemCheckStyle.Checked;
item.Checked = _textView.UseSameRuneTypeForWords;
item.Action += () => _textView.UseSameRuneTypeForWords = (bool)(item.Checked = !item.Checked);
return item;
}
private MenuItem CreateReadOnlyChecked () private MenuItem CreateReadOnlyChecked ()
{ {
var item = new MenuItem { Title = "Read Only" }; var item = new MenuItem { Title = "Read Only" };

View File

@@ -570,6 +570,19 @@ public class TextField : View, IDesignable
/// </summary> /// </summary>
public bool Used { get; set; } public bool Used { get; set; }
/// <summary>
/// Gets or sets whether the word forward and word backward navigation should use the same or equivalent rune type.
/// Default is <c>false</c> meaning using equivalent rune type.
/// </summary>
public bool UseSameRuneTypeForWords { get; set; }
/// <summary>
/// Gets or sets whether the word navigation should select only the word itself without spaces around it or with the
/// spaces at right.
/// Default is <c>false</c> meaning that the spaces at right are included in the selection.
/// </summary>
public bool SelectWordOnlyOnDoubleClick { get; set; }
/// <summary>Clear the selected text.</summary> /// <summary>Clear the selected text.</summary>
public void ClearAllSelection () public void ClearAllSelection ()
{ {
@@ -754,7 +767,7 @@ public class TextField : View, IDesignable
public virtual void KillWordBackwards () public virtual void KillWordBackwards ()
{ {
ClearAllSelection (); ClearAllSelection ();
(int col, int row)? newPos = GetModel ().WordBackward (_cursorPosition, 0); (int col, int row)? newPos = GetModel ().WordBackward (_cursorPosition, 0, UseSameRuneTypeForWords);
if (newPos is null) if (newPos is null)
{ {
@@ -777,7 +790,7 @@ public class TextField : View, IDesignable
public virtual void KillWordForwards () public virtual void KillWordForwards ()
{ {
ClearAllSelection (); ClearAllSelection ();
(int col, int row)? newPos = GetModel ().WordForward (_cursorPosition, 0); (int col, int row)? newPos = GetModel ().WordForward (_cursorPosition, 0, UseSameRuneTypeForWords);
if (newPos is null) if (newPos is null)
{ {
@@ -857,43 +870,15 @@ public class TextField : View, IDesignable
{ {
EnsureHasFocus (); EnsureHasFocus ();
int x = PositionCursor (ev); int x = PositionCursor (ev);
int sbw = x; (int startCol, int col, int row)? newPos = GetModel ().ProcessDoubleClickSelection (x, x, 0, UseSameRuneTypeForWords, SelectWordOnlyOnDoubleClick);
if (x == _text.Count if (newPos is null)
|| (x > 0 && (char)_text [x - 1].Value != ' ')
|| (x > 0 && (char)_text [x].Value == ' '))
{
(int col, int row)? newPosBw = GetModel ().WordBackward (x, 0);
if (newPosBw is null)
{
return true;
}
sbw = newPosBw.Value.col;
}
if (sbw != -1)
{
x = sbw;
PositionCursor (x);
}
(int col, int row)? newPosFw = GetModel ().WordForward (x, 0);
if (newPosFw is null)
{ {
return true; return true;
} }
ClearAllSelection (); SelectedStart = newPos.Value.startCol;
CursorPosition = newPos.Value.col;
if (newPosFw.Value.col != -1 && sbw != -1)
{
_cursorPosition = newPosFw.Value.col;
}
PrepareSelection (sbw, newPosFw.Value.col - sbw);
} }
else if (ev.Flags == MouseFlags.Button1TripleClicked) else if (ev.Flags == MouseFlags.Button1TripleClicked)
{ {
@@ -1502,7 +1487,7 @@ public class TextField : View, IDesignable
private void MoveWordLeft () private void MoveWordLeft ()
{ {
ClearAllSelection (); ClearAllSelection ();
(int col, int row)? newPos = GetModel ().WordBackward (_cursorPosition, 0); (int col, int row)? newPos = GetModel ().WordBackward (_cursorPosition, 0, UseSameRuneTypeForWords);
if (newPos is null) if (newPos is null)
{ {
@@ -1528,7 +1513,7 @@ public class TextField : View, IDesignable
if (x > 0) if (x > 0)
{ {
(int col, int row)? newPos = GetModel ().WordBackward (x, 0); (int col, int row)? newPos = GetModel ().WordBackward (x, 0, UseSameRuneTypeForWords);
if (newPos is null) if (newPos is null)
{ {
@@ -1548,7 +1533,7 @@ public class TextField : View, IDesignable
private void MoveWordRight () private void MoveWordRight ()
{ {
ClearAllSelection (); ClearAllSelection ();
(int col, int row)? newPos = GetModel ().WordForward (_cursorPosition, 0); (int col, int row)? newPos = GetModel ().WordForward (_cursorPosition, 0, UseSameRuneTypeForWords);
if (newPos is null) if (newPos is null)
{ {
@@ -1568,7 +1553,7 @@ public class TextField : View, IDesignable
if (_cursorPosition < _text.Count) if (_cursorPosition < _text.Count)
{ {
int x = _start > -1 && _start > _cursorPosition ? _start : _cursorPosition; int x = _start > -1 && _start > _cursorPosition ? _start : _cursorPosition;
(int col, int row)? newPos = GetModel ().WordForward (x, 0); (int col, int row)? newPos = GetModel ().WordForward (x, 0, UseSameRuneTypeForWords);
if (newPos is null) if (newPos is null)
{ {

View File

@@ -206,7 +206,7 @@ internal class TextModel
return sb.ToString (); return sb.ToString ();
} }
public (int col, int row)? WordBackward (int fromCol, int fromRow) public (int col, int row)? WordBackward (int fromCol, int fromRow, bool useSameRuneType)
{ {
if (fromRow == 0 && fromCol == 0) if (fromRow == 0 && fromCol == 0)
{ {
@@ -245,7 +245,7 @@ internal class TextModel
RuneType runeType = GetRuneType (rune); RuneType runeType = GetRuneType (rune);
int lastValidCol = IsSameRuneType (rune, runeType) && (Rune.IsLetterOrDigit (rune) || Rune.IsPunctuation (rune) || Rune.IsSymbol (rune)) int lastValidCol = IsSameRuneType (rune, runeType, useSameRuneType) && (Rune.IsLetterOrDigit (rune) || Rune.IsPunctuation (rune) || Rune.IsSymbol (rune))
? col ? col
: -1; : -1;
@@ -253,21 +253,29 @@ internal class TextModel
{ {
if (Rune.IsWhiteSpace (nRune)) if (Rune.IsWhiteSpace (nRune))
{ {
while (MovePrev (ref nCol, ref nRow, out nRune)) while (MovePrev (ref nCol, ref nRow, out nRune, useSameRuneType))
{ {
lastValidCol = nCol;
if (Rune.IsLetterOrDigit (nRune) || Rune.IsPunctuation (nRune) || Rune.IsSymbol (nRune)) if (Rune.IsLetterOrDigit (nRune) || Rune.IsPunctuation (nRune) || Rune.IsSymbol (nRune))
{ {
lastValidCol = nCol; rune = nRune;
runeType = GetRuneType (nRune);
if (runeType == RuneType.IsWhiteSpace || runeType == RuneType.IsUnknown)
{
runeType = GetRuneType (nRune);
}
break;
} }
} }
if (lastValidCol > -1)
{
nCol = lastValidCol;
nRow = fromRow;
}
if ((!Rune.IsWhiteSpace (nRune) && Rune.IsWhiteSpace (rune))
|| (Rune.IsWhiteSpace (nRune) && !Rune.IsWhiteSpace (rune)))
{
return;
}
if (nRow != fromRow && (Rune.IsLetterOrDigit (nRune) || Rune.IsPunctuation (nRune) || Rune.IsSymbol (nRune))) if (nRow != fromRow && (Rune.IsLetterOrDigit (nRune) || Rune.IsPunctuation (nRune) || Rune.IsSymbol (nRune)))
{ {
List<Cell> line = GetLine (nRow); List<Cell> line = GetLine (nRow);
@@ -276,38 +284,18 @@ internal class TextModel
{ {
nCol = lastValidCol + Math.Max (lastValidCol, line.Count); nCol = lastValidCol + Math.Max (lastValidCol, line.Count);
} }
return;
}
while (MovePrev (ref nCol, ref nRow, out nRune))
{
if (!Rune.IsLetterOrDigit (nRune) && !Rune.IsPunctuation (nRune) && !Rune.IsSymbol (nRune))
{
break;
}
if (nRow != fromRow)
{
break;
}
lastValidCol =
(IsSameRuneType (nRune, runeType) && Rune.IsLetterOrDigit (nRune)) || Rune.IsPunctuation (nRune) || Rune.IsSymbol (nRune)
? nCol
: lastValidCol;
}
if (lastValidCol > -1)
{
nCol = lastValidCol;
nRow = fromRow;
} }
} }
else else
{ {
if (!MovePrev (ref nCol, ref nRow, out nRune)) if (!MovePrev (ref nCol, ref nRow, out nRune, useSameRuneType))
{ {
if (lastValidCol > -1)
{
nCol = lastValidCol;
nRow = fromRow;
}
return; return;
} }
@@ -321,7 +309,7 @@ internal class TextModel
} }
lastValidCol = lastValidCol =
(IsSameRuneType (nRune, runeType) && Rune.IsLetterOrDigit (nRune)) || Rune.IsPunctuation (nRune) || Rune.IsSymbol (nRune) (IsSameRuneType (nRune, runeType, useSameRuneType) && Rune.IsLetterOrDigit (nRune)) || Rune.IsPunctuation (nRune) || Rune.IsSymbol (nRune)
? nCol ? nCol
: lastValidCol; : lastValidCol;
@@ -350,6 +338,15 @@ internal class TextModel
return (col, row); return (col, row);
} }
if (fromCol == col && fromRow == row && row > 0)
{
row--;
List<Cell> line = GetLine (row);
col = line.Count;
return (col, row);
}
return null; return null;
} }
catch (Exception) catch (Exception)
@@ -358,7 +355,7 @@ internal class TextModel
} }
} }
public (int col, int row)? WordForward (int fromCol, int fromRow) public (int col, int row)? WordForward (int fromCol, int fromRow, bool useSameRuneType)
{ {
if (fromRow == _lines.Count - 1 && fromCol == GetLine (_lines.Count - 1).Count) if (fromRow == _lines.Count - 1 && fromCol == GetLine (_lines.Count - 1).Count)
{ {
@@ -370,10 +367,10 @@ internal class TextModel
try try
{ {
Rune rune = RuneAt (col, row)!.Value.Rune; Rune rune = _lines [row].Count > 0 ? RuneAt (col, row)!.Value.Rune : default (Rune);
RuneType runeType = GetRuneType (rune); RuneType runeType = GetRuneType (rune);
int lastValidCol = IsSameRuneType (rune, runeType) && (Rune.IsLetterOrDigit (rune) || Rune.IsPunctuation (rune) || Rune.IsSymbol (rune)) int lastValidCol = IsSameRuneType (rune, runeType, useSameRuneType) && (Rune.IsLetterOrDigit (rune) || Rune.IsPunctuation (rune) || Rune.IsSymbol (rune))
? col ? col
: -1; : -1;
@@ -381,16 +378,23 @@ internal class TextModel
{ {
if (Rune.IsWhiteSpace (nRune)) if (Rune.IsWhiteSpace (nRune))
{ {
while (MoveNext (ref nCol, ref nRow, out nRune)) while (MoveNext (ref nCol, ref nRow, out nRune, useSameRuneType))
{ {
lastValidCol = nCol;
if (Rune.IsLetterOrDigit (nRune) || Rune.IsPunctuation (nRune) || Rune.IsSymbol (nRune)) if (Rune.IsLetterOrDigit (nRune) || Rune.IsPunctuation (nRune) || Rune.IsSymbol (nRune))
{ {
lastValidCol = nCol;
return; return;
} }
} }
lastValidCol = nCol;
if (!Rune.IsWhiteSpace (nRune) && Rune.IsWhiteSpace (rune))
{
return;
}
if (nRow != fromRow && (Rune.IsLetterOrDigit (nRune) || Rune.IsPunctuation (nRune) || Rune.IsSymbol (nRune))) if (nRow != fromRow && (Rune.IsLetterOrDigit (nRune) || Rune.IsPunctuation (nRune) || Rune.IsSymbol (nRune)))
{ {
if (lastValidCol > -1) if (lastValidCol > -1)
@@ -401,24 +405,6 @@ internal class TextModel
return; return;
} }
while (MoveNext (ref nCol, ref nRow, out nRune))
{
if (!Rune.IsLetterOrDigit (nRune) && !Rune.IsPunctuation (nRune) && !Rune.IsSymbol (nRune))
{
break;
}
if (nRow != fromRow)
{
break;
}
lastValidCol =
(IsSameRuneType (nRune, runeType) && Rune.IsLetterOrDigit (nRune)) || Rune.IsPunctuation (nRune) || Rune.IsSymbol (nRune)
? nCol
: lastValidCol;
}
if (lastValidCol > -1) if (lastValidCol > -1)
{ {
nCol = lastValidCol; nCol = lastValidCol;
@@ -427,12 +413,14 @@ internal class TextModel
} }
else else
{ {
if (!MoveNext (ref nCol, ref nRow, out nRune)) if (!MoveNext (ref nCol, ref nRow, out nRune, useSameRuneType))
{ {
return; return;
} }
if (!IsSameRuneType (nRune, runeType) && !Rune.IsWhiteSpace (nRune)) lastValidCol = nCol;
if (!IsSameRuneType (nRune, runeType, useSameRuneType) && !Rune.IsWhiteSpace (nRune))
{ {
return; return;
} }
@@ -446,11 +434,6 @@ internal class TextModel
return; return;
} }
lastValidCol =
(IsSameRuneType (nRune, runeType) && Rune.IsLetterOrDigit (nRune)) || Rune.IsPunctuation (nRune) || Rune.IsSymbol (nRune)
? nCol
: lastValidCol;
if (fromRow != nRow) if (fromRow != nRow)
{ {
nCol = 0; nCol = 0;
@@ -477,6 +460,64 @@ internal class TextModel
} }
} }
public (int startCol, int col, int row)? ProcessDoubleClickSelection (int fromStartCol, int fromCol, int fromRow, bool useSameRuneType, bool selectWordOnly)
{
List<Cell> line = GetLine (fromRow);
int startCol = fromStartCol;
int col = fromCol;
int row = fromRow;
(int col, int row)? newPos = WordForward (col, row, useSameRuneType);
if (newPos.HasValue)
{
col = row == newPos.Value.row ? newPos.Value.col : 0;
}
if (startCol > 0
&& StringExtensions.ToString (line.GetRange (startCol, col - startCol).Select (c => c.Rune).ToList ()).Trim () == ""
&& (col - startCol > 1 || (col - startCol > 0 && line [startCol - 1].Rune == (Rune)' ')))
{
while (startCol > 0 && line [startCol - 1].Rune == (Rune)' ')
{
startCol--;
}
}
else
{
newPos = WordBackward (col, row, useSameRuneType);
if (newPos is { })
{
startCol = row == newPos.Value.row ? newPos.Value.col : line.Count;
}
}
if (selectWordOnly)
{
List<Rune> selRunes = line.GetRange (startCol, col - startCol).Select (c => c.Rune).ToList ();
if (StringExtensions.ToString (selRunes).Trim () != "")
{
for (int i = selRunes.Count - 1; i > -1; i--)
{
if (selRunes [i] == (Rune)' ')
{
col--;
}
}
}
}
if (fromStartCol != startCol || fromCol != col || fromRow != row)
{
return (startCol, col, row);
}
return null;
}
internal static int CalculateLeftColumn (List<Cell> t, int start, int end, int width, int tabWidth = 0) internal static int CalculateLeftColumn (List<Cell> t, int start, int end, int width, int tabWidth = 0)
{ {
List<Rune> runes = new (); List<Rune> runes = new ();
@@ -966,11 +1007,27 @@ internal class TextModel
return RuneType.IsUnknown; return RuneType.IsUnknown;
} }
private bool IsSameRuneType (Rune newRune, RuneType runeType) private bool IsSameRuneType (Rune newRune, RuneType runeType, bool useSameRuneType)
{ {
RuneType rt = GetRuneType (newRune); RuneType rt = GetRuneType (newRune);
return rt == runeType; if (useSameRuneType)
{
return rt == runeType;
}
switch (runeType)
{
case RuneType.IsSymbol:
case RuneType.IsPunctuation:
return rt is RuneType.IsSymbol or RuneType.IsPunctuation;
case RuneType.IsWhiteSpace:
case RuneType.IsLetterOrDigit:
case RuneType.IsUnknown:
return rt == runeType;
default:
throw new ArgumentOutOfRangeException (nameof (runeType), runeType, null);
}
} }
private bool MatchWholeWord (string source, string matchText, int index = 0) private bool MatchWholeWord (string source, string matchText, int index = 0)
@@ -992,7 +1049,7 @@ internal class TextModel
return false; return false;
} }
private bool MoveNext (ref int col, ref int row, out Rune rune) private bool MoveNext (ref int col, ref int row, out Rune rune, bool useSameRuneType)
{ {
List<Cell> line = GetLine (row); List<Cell> line = GetLine (row);
@@ -1001,39 +1058,40 @@ internal class TextModel
col++; col++;
rune = line [col].Rune; rune = line [col].Rune;
if (col + 1 == line.Count && !Rune.IsLetterOrDigit (rune) && !Rune.IsWhiteSpace (line [col - 1].Rune)) if (col + 1 == line.Count
&& !Rune.IsLetterOrDigit (rune)
&& !Rune.IsWhiteSpace (line [col - 1].Rune)
&& IsSameRuneType (line [col - 1].Rune, GetRuneType (rune), useSameRuneType))
{ {
col++; col++;
} }
if (!Rune.IsWhiteSpace (rune)
&& (Rune.IsWhiteSpace (line [col - 1].Rune) || !IsSameRuneType (line [col - 1].Rune, GetRuneType (rune), useSameRuneType)))
{
return false;
}
return true; return true;
} }
if (col + 1 == line.Count) if (col + 1 == line.Count)
{ {
col++; col++;
rune = default (Rune);
return false;
} }
while (row + 1 < Count) // End of line
{ col = 0;
col = 0; row++;
row++;
line = GetLine (row);
if (line.Count > 0)
{
rune = line [0].Rune;
return true;
}
}
rune = default (Rune); rune = default (Rune);
return false; return false;
} }
private bool MovePrev (ref int col, ref int row, out Rune rune) private bool MovePrev (ref int col, ref int row, out Rune rune, bool useSameRuneType)
{ {
List<Cell> line = GetLine (row); List<Cell> line = GetLine (row);
@@ -1042,28 +1100,15 @@ internal class TextModel
col--; col--;
rune = line [col].Rune; rune = line [col].Rune;
return true; if ((!Rune.IsWhiteSpace (rune)
} && !Rune.IsWhiteSpace (line [col + 1].Rune)
&& !IsSameRuneType (line [col + 1].Rune, GetRuneType (rune), useSameRuneType))
if (row == 0) || (Rune.IsWhiteSpace (rune) && !Rune.IsWhiteSpace (line [col + 1].Rune)))
{
rune = default (Rune);
return false;
}
while (row > 0)
{
row--;
line = GetLine (row);
col = line.Count - 1;
if (col >= 0)
{ {
rune = line [col].Rune; return false;
return true;
} }
return true;
} }
rune = default (Rune); rune = default (Rune);

View File

@@ -1015,6 +1015,19 @@ public class TextView : View, IDesignable
} }
} }
/// <summary>
/// Gets or sets whether the word forward and word backward navigation should use the same or equivalent rune type.
/// Default is <c>false</c> meaning using equivalent rune type.
/// </summary>
public bool UseSameRuneTypeForWords { get; set; }
/// <summary>
/// Gets or sets whether the word navigation should select only the word itself without spaces around it or with the
/// spaces at right.
/// Default is <c>false</c> meaning that the spaces at right are included in the selection.
/// </summary>
public bool SelectWordOnlyOnDoubleClick { get; set; }
/// <summary>Allows clearing the <see cref="HistoryTextItemEventArgs"/> items updating the original text.</summary> /// <summary>Allows clearing the <see cref="HistoryTextItemEventArgs"/> items updating the original text.</summary>
public void ClearHistoryChanges () { _historyText?.Clear (_model.GetAllLines ()); } public void ClearHistoryChanges () { _historyText?.Clear (_model.GetAllLines ()); }
@@ -1689,29 +1702,19 @@ public class TextView : View, IDesignable
} }
ProcessMouseClick (ev, out List<Cell> line); ProcessMouseClick (ev, out List<Cell> line);
(int col, int row)? newPos;
if (CurrentColumn == line.Count
|| (CurrentColumn > 0 && (line [CurrentColumn - 1].Rune.Value != ' ' || line [CurrentColumn].Rune.Value == ' ')))
{
newPos = _model.WordBackward (CurrentColumn, CurrentRow);
if (newPos.HasValue)
{
CurrentColumn = CurrentRow == newPos.Value.row ? newPos.Value.col : 0;
}
}
if (!IsSelecting) if (!IsSelecting)
{ {
StartSelecting (); StartSelecting ();
} }
newPos = _model.WordForward (CurrentColumn, CurrentRow); (int startCol, int col, int row)? newPos = _model.ProcessDoubleClickSelection (SelectionStartColumn, CurrentColumn, CurrentRow, UseSameRuneTypeForWords, SelectWordOnlyOnDoubleClick);
if (newPos is { } && newPos.HasValue) if (newPos.HasValue)
{ {
CurrentColumn = CurrentRow == newPos.Value.row ? newPos.Value.col : line.Count; SelectionStartColumn = newPos.Value.startCol;
CurrentColumn = newPos.Value.col;
CurrentRow = newPos.Value.row;
} }
PositionCursor (); PositionCursor ();
@@ -3380,7 +3383,7 @@ public class TextView : View, IDesignable
return; return;
} }
(int col, int row)? newPos = _model.WordBackward (CurrentColumn, CurrentRow); (int col, int row)? newPos = _model.WordBackward (CurrentColumn, CurrentRow, UseSameRuneTypeForWords);
if (newPos.HasValue && CurrentRow == newPos.Value.row) if (newPos.HasValue && CurrentRow == newPos.Value.row)
{ {
@@ -3464,7 +3467,7 @@ public class TextView : View, IDesignable
return; return;
} }
(int col, int row)? newPos = _model.WordForward (CurrentColumn, CurrentRow); (int col, int row)? newPos = _model.WordForward (CurrentColumn, CurrentRow, UseSameRuneTypeForWords);
var restCount = 0; var restCount = 0;
if (newPos.HasValue && CurrentRow == newPos.Value.row) if (newPos.HasValue && CurrentRow == newPos.Value.row)
@@ -3754,7 +3757,7 @@ public class TextView : View, IDesignable
private void MoveWordBackward () private void MoveWordBackward ()
{ {
(int col, int row)? newPos = _model.WordBackward (CurrentColumn, CurrentRow); (int col, int row)? newPos = _model.WordBackward (CurrentColumn, CurrentRow, UseSameRuneTypeForWords);
if (newPos.HasValue) if (newPos.HasValue)
{ {
@@ -3767,7 +3770,7 @@ public class TextView : View, IDesignable
private void MoveWordForward () private void MoveWordForward ()
{ {
(int col, int row)? newPos = _model.WordForward (CurrentColumn, CurrentRow); (int col, int row)? newPos = _model.WordForward (CurrentColumn, CurrentRow, UseSameRuneTypeForWords);
if (newPos.HasValue) if (newPos.HasValue)
{ {

View File

@@ -607,6 +607,8 @@ public class TextFieldTests (ITestOutputHelper output)
Assert.True (tf.NewKeyDownEvent (Key.CursorRight.WithShift.WithCtrl)); Assert.True (tf.NewKeyDownEvent (Key.CursorRight.WithShift.WithCtrl));
#endif #endif
Assert.Equal ("is is a test.", tf.Text); Assert.Equal ("is is a test.", tf.Text);
Assert.Equal ("is a test", tf.SelectedText);
Assert.True (tf.NewKeyDownEvent (Key.CursorRight.WithShift.WithCtrl));
Assert.Equal ("is a test.", tf.SelectedText); Assert.Equal ("is a test.", tf.SelectedText);
Assert.Equal (13, tf.CursorPosition); Assert.Equal (13, tf.CursorPosition);
Assert.True (tf.NewKeyDownEvent (Key.CursorLeft)); Assert.True (tf.NewKeyDownEvent (Key.CursorLeft));
@@ -1340,6 +1342,13 @@ public class TextFieldTests (ITestOutputHelper output)
break; break;
case 5: case 5:
Assert.Equal (31, _textField.CursorPosition);
Assert.Equal (-1, _textField.SelectedStart);
Assert.Equal (0, _textField.SelectedLength);
Assert.Null (_textField.SelectedText);
break;
case 6:
Assert.Equal (32, _textField.CursorPosition); Assert.Equal (32, _textField.CursorPosition);
Assert.Equal (-1, _textField.SelectedStart); Assert.Equal (-1, _textField.SelectedStart);
Assert.Equal (0, _textField.SelectedLength); Assert.Equal (0, _textField.SelectedLength);
@@ -1501,6 +1510,13 @@ public class TextFieldTests (ITestOutputHelper output)
break; break;
case 5: case 5:
Assert.Equal (31, _textField.CursorPosition);
Assert.Equal (0, _textField.SelectedStart);
Assert.Equal (31, _textField.SelectedLength);
Assert.Equal ("TAB to jump between text fields", _textField.SelectedText);
break;
case 6:
Assert.Equal (32, _textField.CursorPosition); Assert.Equal (32, _textField.CursorPosition);
Assert.Equal (0, _textField.SelectedStart); Assert.Equal (0, _textField.SelectedStart);
Assert.Equal (32, _textField.SelectedLength); Assert.Equal (32, _textField.SelectedLength);
@@ -1550,6 +1566,13 @@ public class TextFieldTests (ITestOutputHelper output)
break; break;
case 3: case 3:
Assert.Equal (31, _textField.CursorPosition);
Assert.Equal (10, _textField.SelectedStart);
Assert.Equal (21, _textField.SelectedLength);
Assert.Equal ("p between text fields", _textField.SelectedText);
break;
case 4:
Assert.Equal (32, _textField.CursorPosition); Assert.Equal (32, _textField.CursorPosition);
Assert.Equal (10, _textField.SelectedStart); Assert.Equal (10, _textField.SelectedStart);
Assert.Equal (22, _textField.SelectedLength); Assert.Equal (22, _textField.SelectedLength);

File diff suppressed because it is too large Load Diff

View File

@@ -348,6 +348,9 @@ public class TextFieldTests
tf.BeginInit (); tf.BeginInit ();
tf.EndInit (); tf.EndInit ();
Assert.False (tf.UseSameRuneTypeForWords);
Assert.Equal (22, tf.CursorPosition);
tf.NewKeyDownEvent (Key.CursorLeft.WithCtrl); tf.NewKeyDownEvent (Key.CursorLeft.WithCtrl);
Assert.Equal (15, tf.CursorPosition); Assert.Equal (15, tf.CursorPosition);
tf.NewKeyDownEvent (Key.CursorLeft.WithCtrl); tf.NewKeyDownEvent (Key.CursorLeft.WithCtrl);
@@ -402,14 +405,14 @@ public class TextFieldTests
new () { Position = new (idx, 1), Flags = MouseFlags.Button1DoubleClicked, View = tf } new () { Position = new (idx, 1), Flags = MouseFlags.Button1DoubleClicked, View = tf }
) )
); );
Assert.Equal ("movie.", tf.SelectedText); Assert.Equal ("movie", tf.SelectedText);
Assert.True ( Assert.True (
tf.NewMouseEvent ( tf.NewMouseEvent (
new () { Position = new (idx + 1, 1), Flags = MouseFlags.Button1DoubleClicked, View = tf } new () { Position = new (idx + 1, 1), Flags = MouseFlags.Button1DoubleClicked, View = tf }
) )
); );
Assert.Equal ("movie.", tf.SelectedText); Assert.Equal ("movie", tf.SelectedText);
} }
[Fact] [Fact]

File diff suppressed because it is too large Load Diff