diff --git a/Terminal.Gui/Views/TextInput/HistoryText.cs b/Terminal.Gui/Views/TextInput/History/HistoryText.cs
similarity index 100%
rename from Terminal.Gui/Views/TextInput/HistoryText.cs
rename to Terminal.Gui/Views/TextInput/History/HistoryText.cs
diff --git a/Terminal.Gui/Views/TextInput/HistoryTextItemEventArgs.cs b/Terminal.Gui/Views/TextInput/History/HistoryTextItemEventArgs.cs
similarity index 100%
rename from Terminal.Gui/Views/TextInput/HistoryTextItemEventArgs.cs
rename to Terminal.Gui/Views/TextInput/History/HistoryTextItemEventArgs.cs
diff --git a/Terminal.Gui/Views/TextInput/TextEditingLineStatus.cs b/Terminal.Gui/Views/TextInput/History/TextEditingLineStatus.cs
similarity index 100%
rename from Terminal.Gui/Views/TextInput/TextEditingLineStatus.cs
rename to Terminal.Gui/Views/TextInput/History/TextEditingLineStatus.cs
diff --git a/Terminal.Gui/Views/TextInput/ITextValidateProvider.cs b/Terminal.Gui/Views/TextInput/TextValidation/ITextValidateProvider.cs
similarity index 100%
rename from Terminal.Gui/Views/TextInput/ITextValidateProvider.cs
rename to Terminal.Gui/Views/TextInput/TextValidation/ITextValidateProvider.cs
diff --git a/Terminal.Gui/Views/TextInput/NetMaskedTextProvider.cs b/Terminal.Gui/Views/TextInput/TextValidation/NetMaskedTextProvider.cs
similarity index 100%
rename from Terminal.Gui/Views/TextInput/NetMaskedTextProvider.cs
rename to Terminal.Gui/Views/TextInput/TextValidation/NetMaskedTextProvider.cs
diff --git a/Terminal.Gui/Views/TextInput/TextRegexProvider.cs b/Terminal.Gui/Views/TextInput/TextValidation/TextRegexProvider.cs
similarity index 100%
rename from Terminal.Gui/Views/TextInput/TextRegexProvider.cs
rename to Terminal.Gui/Views/TextInput/TextValidation/TextRegexProvider.cs
diff --git a/Terminal.Gui/Views/TextInput/TextValidateField.cs b/Terminal.Gui/Views/TextInput/TextValidation/TextValidateField.cs
similarity index 100%
rename from Terminal.Gui/Views/TextInput/TextValidateField.cs
rename to Terminal.Gui/Views/TextInput/TextValidation/TextValidateField.cs
diff --git a/Terminal.Gui/Views/TextInput/TextView.cs b/Terminal.Gui/Views/TextInput/TextView.cs
deleted file mode 100644
index 5e2124d38e..0000000000
--- a/Terminal.Gui/Views/TextInput/TextView.cs
+++ /dev/null
@@ -1,4803 +0,0 @@
-using System.Globalization;
-using System.Runtime.CompilerServices;
-
-namespace Terminal.Gui.Views;
-
-/// Fully featured multi-line text editor
-///
-///
-///
-/// Shortcut Action performed
-///
-/// -
-/// Left cursor, Control-b Moves the editing point left.
-///
-/// -
-/// Right cursor, Control-f Moves the editing point right.
-///
-/// -
-/// Alt-b Moves one word back.
-///
-/// -
-/// Alt-f Moves one word forward.
-///
-/// -
-/// Up cursor, Control-p Moves the editing point one line up.
-///
-/// -
-/// Down cursor, Control-n Moves the editing point one line down
-///
-/// -
-/// Home key, Control-a Moves the cursor to the beginning of the line.
-///
-/// -
-/// End key, Control-e Moves the cursor to the end of the line.
-///
-/// -
-/// Control-Home Scrolls to the first line and moves the cursor there.
-///
-/// -
-/// Control-End Scrolls to the last line and moves the cursor there.
-///
-/// -
-/// Delete, Control-d Deletes the character in front of the cursor.
-///
-/// -
-/// Backspace Deletes the character behind the cursor.
-///
-/// -
-/// Control-k
-///
-/// Deletes the text until the end of the line and replaces the kill buffer with the deleted text.
-/// You can paste this text in a different place by using Control-y.
-///
-///
-/// -
-/// Control-y
-/// Pastes the content of the kill ring into the current position.
-///
-/// -
-/// Alt-d
-///
-/// Deletes the word above the cursor and adds it to the kill ring. You can paste the contents of
-/// the kill ring with Control-y.
-///
-///
-/// -
-/// Control-q
-///
-/// Quotes the next input character, to prevent the normal processing of key handling to take
-/// place.
-///
-///
-///
-///
-public class TextView : View, IDesignable
-{
- private readonly HistoryText _historyText = new ();
- private bool _allowsReturn = true;
- private bool _allowsTab = true;
- private bool _clickWithSelecting;
-
- // The column we are tracking, or -1 if we are not tracking any column
- private int _columnTrack = -1;
- private bool _continuousFind;
- private bool _copyWithoutSelection;
- private string? _currentCaller;
- private CultureInfo? _currentCulture;
- private bool _isButtonShift;
- private bool _isButtonReleased;
- private bool _isDrawing;
- private bool _isReadOnly;
- private bool _lastWasKill;
- private int _leftColumn;
- private TextModel _model = new ();
- private bool _multiline = true;
- private Dim? _savedHeight;
- private int _selectionStartColumn, _selectionStartRow;
- private bool _shiftSelecting;
- private int _tabWidth = 4;
- private int _topRow;
- private bool _wordWrap;
- private WordWrapManager? _wrapManager;
- private bool _wrapNeeded;
-
- ///
- /// Initializes a on the specified area, with dimensions controlled with the X, Y, Width
- /// and Height properties.
- ///
- public TextView ()
- {
- CanFocus = true;
- CursorVisibility = CursorVisibility.Default;
- Used = true;
-
- // By default, disable hotkeys (in case someone sets Title)
- base.HotKeySpecifier = new ('\xffff');
-
- _model.LinesLoaded += Model_LinesLoaded!;
- _historyText.ChangeText += HistoryText_ChangeText!;
-
- Initialized += TextView_Initialized!;
-
- SuperViewChanged += TextView_SuperViewChanged!;
-
- SubViewsLaidOut += TextView_LayoutComplete;
-
- // Things this view knows how to do
-
- // Note - NewLine is only bound to Enter if Multiline is true
- AddCommand (Command.NewLine, ctx => ProcessEnterKey (ctx));
-
- AddCommand (
- Command.PageDown,
- () =>
- {
- ProcessPageDown ();
-
- return true;
- }
- );
-
- AddCommand (
- Command.PageDownExtend,
- () =>
- {
- ProcessPageDownExtend ();
-
- return true;
- }
- );
-
- AddCommand (
- Command.PageUp,
- () =>
- {
- ProcessPageUp ();
-
- return true;
- }
- );
-
- AddCommand (
- Command.PageUpExtend,
- () =>
- {
- ProcessPageUpExtend ();
-
- return true;
- }
- );
-
- AddCommand (Command.Down, () => ProcessMoveDown ());
-
- AddCommand (
- Command.DownExtend,
- () =>
- {
- ProcessMoveDownExtend ();
-
- return true;
- }
- );
-
- AddCommand (Command.Up, () => ProcessMoveUp ());
-
- AddCommand (
- Command.UpExtend,
- () =>
- {
- ProcessMoveUpExtend ();
-
- return true;
- }
- );
- AddCommand (Command.Right, () => ProcessMoveRight ());
-
- AddCommand (
- Command.RightExtend,
- () =>
- {
- ProcessMoveRightExtend ();
-
- return true;
- }
- );
- AddCommand (Command.Left, () => ProcessMoveLeft ());
-
- AddCommand (
- Command.LeftExtend,
- () =>
- {
- ProcessMoveLeftExtend ();
-
- return true;
- }
- );
-
- AddCommand (
- Command.DeleteCharLeft,
- () =>
- {
- ProcessDeleteCharLeft ();
-
- return true;
- }
- );
-
- AddCommand (
- Command.LeftStart,
- () =>
- {
- ProcessMoveLeftStart ();
-
- return true;
- }
- );
-
- AddCommand (
- Command.LeftStartExtend,
- () =>
- {
- ProcessMoveLeftStartExtend ();
-
- return true;
- }
- );
-
- AddCommand (
- Command.DeleteCharRight,
- () =>
- {
- ProcessDeleteCharRight ();
-
- return true;
- }
- );
-
- AddCommand (
- Command.RightEnd,
- () =>
- {
- ProcessMoveEndOfLine ();
-
- return true;
- }
- );
-
- AddCommand (
- Command.RightEndExtend,
- () =>
- {
- ProcessMoveRightEndExtend ();
-
- return true;
- }
- );
-
- AddCommand (
- Command.CutToEndLine,
- () =>
- {
- KillToEndOfLine ();
-
- return true;
- }
- );
-
- AddCommand (
- Command.CutToStartLine,
- () =>
- {
- KillToLeftStart ();
-
- return true;
- }
- );
-
- AddCommand (
- Command.Paste,
- () =>
- {
- ProcessPaste ();
-
- return true;
- }
- );
-
- AddCommand (
- Command.ToggleExtend,
- () =>
- {
- ToggleSelecting ();
-
- return true;
- }
- );
-
- AddCommand (
- Command.Copy,
- () =>
- {
- ProcessCopy ();
-
- return true;
- }
- );
-
- AddCommand (
- Command.Cut,
- () =>
- {
- ProcessCut ();
-
- return true;
- }
- );
-
- AddCommand (
- Command.WordLeft,
- () =>
- {
- ProcessMoveWordBackward ();
-
- return true;
- }
- );
-
- AddCommand (
- Command.WordLeftExtend,
- () =>
- {
- ProcessMoveWordBackwardExtend ();
-
- return true;
- }
- );
-
- AddCommand (
- Command.WordRight,
- () =>
- {
- ProcessMoveWordForward ();
-
- return true;
- }
- );
-
- AddCommand (
- Command.WordRightExtend,
- () =>
- {
- ProcessMoveWordForwardExtend ();
-
- return true;
- }
- );
-
- AddCommand (
- Command.KillWordForwards,
- () =>
- {
- ProcessKillWordForward ();
-
- return true;
- }
- );
-
- AddCommand (
- Command.KillWordBackwards,
- () =>
- {
- ProcessKillWordBackward ();
-
- return true;
- }
- );
-
- AddCommand (
- Command.End,
- () =>
- {
- MoveBottomEnd ();
-
- return true;
- }
- );
-
- AddCommand (
- Command.EndExtend,
- () =>
- {
- MoveBottomEndExtend ();
-
- return true;
- }
- );
-
- AddCommand (
- Command.Start,
- () =>
- {
- MoveTopHome ();
-
- return true;
- }
- );
-
- AddCommand (
- Command.StartExtend,
- () =>
- {
- MoveTopHomeExtend ();
-
- return true;
- }
- );
-
- AddCommand (
- Command.SelectAll,
- () =>
- {
- ProcessSelectAll ();
-
- return true;
- }
- );
-
- AddCommand (
- Command.ToggleOverwrite,
- () =>
- {
- ProcessSetOverwrite ();
-
- return true;
- }
- );
-
- AddCommand (
- Command.EnableOverwrite,
- () =>
- {
- SetOverwrite (true);
-
- return true;
- }
- );
-
- AddCommand (
- Command.DisableOverwrite,
- () =>
- {
- SetOverwrite (false);
-
- return true;
- }
- );
- AddCommand (Command.Tab, () => ProcessTab ());
- AddCommand (Command.BackTab, () => ProcessBackTab ());
-
- AddCommand (
- Command.Undo,
- () =>
- {
- Undo ();
-
- return true;
- }
- );
-
- AddCommand (
- Command.Redo,
- () =>
- {
- Redo ();
-
- return true;
- }
- );
-
- AddCommand (
- Command.DeleteAll,
- () =>
- {
- DeleteAll ();
-
- return true;
- }
- );
-
- AddCommand (
- Command.Context,
- () =>
- {
- ShowContextMenu (null);
-
- return true;
- }
- );
-
- AddCommand (
- Command.Open,
- () =>
- {
- PromptForColors ();
-
- return true;
- });
-
- // Default keybindings for this view
- KeyBindings.Remove (Key.Space);
-
- KeyBindings.Remove (Key.Enter);
- KeyBindings.Add (Key.Enter, Multiline ? Command.NewLine : Command.Accept);
-
- KeyBindings.Add (Key.PageDown, Command.PageDown);
- KeyBindings.Add (Key.V.WithCtrl, Command.PageDown);
-
- KeyBindings.Add (Key.PageDown.WithShift, Command.PageDownExtend);
-
- KeyBindings.Add (Key.PageUp, Command.PageUp);
-
- KeyBindings.Add (Key.PageUp.WithShift, Command.PageUpExtend);
-
- KeyBindings.Add (Key.N.WithCtrl, Command.Down);
- KeyBindings.Add (Key.CursorDown, Command.Down);
-
- KeyBindings.Add (Key.CursorDown.WithShift, Command.DownExtend);
-
- KeyBindings.Add (Key.P.WithCtrl, Command.Up);
- KeyBindings.Add (Key.CursorUp, Command.Up);
-
- KeyBindings.Add (Key.CursorUp.WithShift, Command.UpExtend);
-
- KeyBindings.Add (Key.F.WithCtrl, Command.Right);
- KeyBindings.Add (Key.CursorRight, Command.Right);
-
- KeyBindings.Add (Key.CursorRight.WithShift, Command.RightExtend);
-
- KeyBindings.Add (Key.B.WithCtrl, Command.Left);
- KeyBindings.Add (Key.CursorLeft, Command.Left);
-
- KeyBindings.Add (Key.CursorLeft.WithShift, Command.LeftExtend);
-
- KeyBindings.Add (Key.Backspace, Command.DeleteCharLeft);
-
- KeyBindings.Add (Key.Home, Command.LeftStart);
-
- KeyBindings.Add (Key.Home.WithShift, Command.LeftStartExtend);
-
- KeyBindings.Add (Key.Delete, Command.DeleteCharRight);
- KeyBindings.Add (Key.D.WithCtrl, Command.DeleteCharRight);
-
- KeyBindings.Add (Key.End, Command.RightEnd);
- KeyBindings.Add (Key.E.WithCtrl, Command.RightEnd);
-
- KeyBindings.Add (Key.End.WithShift, Command.RightEndExtend);
-
- KeyBindings.Add (Key.K.WithCtrl, Command.CutToEndLine); // kill-to-end
-
- KeyBindings.Add (Key.Delete.WithCtrl.WithShift, Command.CutToEndLine); // kill-to-end
-
- KeyBindings.Add (Key.Backspace.WithCtrl.WithShift, Command.CutToStartLine); // kill-to-start
-
- KeyBindings.Add (Key.Y.WithCtrl, Command.Paste); // Control-y, yank
- KeyBindings.Add (Key.Space.WithCtrl, Command.ToggleExtend);
-
- KeyBindings.Add (Key.C.WithCtrl, Command.Copy);
-
- KeyBindings.Add (Key.W.WithCtrl, Command.Cut); // Move to Unix?
- KeyBindings.Add (Key.X.WithCtrl, Command.Cut);
-
- KeyBindings.Add (Key.CursorLeft.WithCtrl, Command.WordLeft);
-
- KeyBindings.Add (Key.CursorLeft.WithCtrl.WithShift, Command.WordLeftExtend);
-
- KeyBindings.Add (Key.CursorRight.WithCtrl, Command.WordRight);
-
- KeyBindings.Add (Key.CursorRight.WithCtrl.WithShift, Command.WordRightExtend);
- KeyBindings.Add (Key.Delete.WithCtrl, Command.KillWordForwards); // kill-word-forwards
- KeyBindings.Add (Key.Backspace.WithCtrl, Command.KillWordBackwards); // kill-word-backwards
-
- KeyBindings.Add (Key.End.WithCtrl, Command.End);
- KeyBindings.Add (Key.End.WithCtrl.WithShift, Command.EndExtend);
- KeyBindings.Add (Key.Home.WithCtrl, Command.Start);
- KeyBindings.Add (Key.Home.WithCtrl.WithShift, Command.StartExtend);
- KeyBindings.Add (Key.A.WithCtrl, Command.SelectAll);
- KeyBindings.Add (Key.InsertChar, Command.ToggleOverwrite);
- KeyBindings.Add (Key.Tab, Command.Tab);
- KeyBindings.Add (Key.Tab.WithShift, Command.BackTab);
-
- KeyBindings.Add (Key.Z.WithCtrl, Command.Undo);
- KeyBindings.Add (Key.R.WithCtrl, Command.Redo);
-
- KeyBindings.Add (Key.G.WithCtrl, Command.DeleteAll);
- KeyBindings.Add (Key.D.WithCtrl.WithShift, Command.DeleteAll);
-
- KeyBindings.Add (Key.L.WithCtrl, Command.Open);
-
-#if UNIX_KEY_BINDINGS
- KeyBindings.Add (Key.C.WithAlt, Command.Copy);
- KeyBindings.Add (Key.B.WithAlt, Command.WordLeft);
- KeyBindings.Add (Key.W.WithAlt, Command.Cut);
- KeyBindings.Add (Key.V.WithAlt, Command.PageUp);
- KeyBindings.Add (Key.F.WithAlt, Command.WordRight);
- KeyBindings.Add (Key.K.WithAlt, Command.CutToStartLine); // kill-to-start
-#endif
-
- _currentCulture = Thread.CurrentThread.CurrentUICulture;
- }
-
- // BUGBUG: AllowsReturn is mis-named. It should be EnterKeyAccepts.
- ///
- /// Gets or sets whether pressing ENTER in a creates a new line of text
- /// in the view or invokes the event.
- ///
- ///
- ///
- /// Setting this property alters .
- /// If is set to , then is also set to
- /// `true` and
- /// vice-versa.
- ///
- ///
- /// If is set to , then gets set to
- /// .
- ///
- ///
- public bool AllowsReturn
- {
- get => _allowsReturn;
- set
- {
- _allowsReturn = value;
-
- if (_allowsReturn && !_multiline)
- {
- // BUGBUG: Setting properties should not have side-effects like this. Multiline and AllowsReturn should be independent.
- Multiline = true;
- }
-
- if (!_allowsReturn && _multiline)
- {
- Multiline = false;
-
- // BUGBUG: Setting properties should not have side-effects like this. Multiline and AllowsTab should be independent.
- AllowsTab = false;
- }
-
- SetNeedsDraw ();
- }
- }
-
- ///
- /// Gets or sets whether the inserts a tab character into the text or ignores tab input. If
- /// set to `false` and the user presses the tab key (or shift-tab) the focus will move to the next view (or previous
- /// with shift-tab). The default is `true`; if the user presses the tab key, a tab character will be inserted into the
- /// text.
- ///
- public bool AllowsTab
- {
- get => _allowsTab;
- set
- {
- _allowsTab = value;
-
- if (_allowsTab && _tabWidth == 0)
- {
- _tabWidth = 4;
- }
-
- if (_allowsTab && !_multiline)
- {
- Multiline = true;
- }
-
- if (!_allowsTab && _tabWidth > 0)
- {
- _tabWidth = 0;
- }
-
- SetNeedsDraw ();
- }
- }
-
- ///
- /// Provides autocomplete context menu based on suggestions at the current cursor position. Configure
- /// to enable this feature
- ///
- public IAutocomplete Autocomplete { get; protected set; } = new TextViewAutocomplete ();
-
- /// Get the Context Menu.
- public PopoverMenu? ContextMenu { get; private set; }
-
- /// Gets the cursor column.
- /// The cursor column.
- public int CurrentColumn { get; private set; }
-
- /// Gets the current cursor row.
- public int CurrentRow { get; private set; }
-
- /// Sets or gets the current cursor position.
- public Point CursorPosition
- {
- get => new (CurrentColumn, CurrentRow);
- set
- {
- List line = _model.GetLine (Math.Max (Math.Min (value.Y, _model.Count - 1), 0));
-
- CurrentColumn = value.X < 0 ? 0 :
- value.X > line.Count ? line.Count : value.X;
-
- CurrentRow = value.Y < 0 ? 0 :
- value.Y > _model.Count - 1 ? Math.Max (_model.Count - 1, 0) : value.Y;
- SetNeedsDraw ();
- Adjust ();
- }
- }
-
- ///
- /// Indicates whatever the text has history changes or not. if the text has history changes
- /// otherwise.
- ///
- public bool HasHistoryChanges => _historyText.HasHistoryChanges;
-
- ///
- /// If and the current is null will inherit from the
- /// previous, otherwise if (default) do nothing. If the text is load with
- /// this property is automatically sets to .
- ///
- public bool InheritsPreviousAttribute { get; set; }
-
- ///
- /// Indicates whatever the text was changed or not. if the text was changed
- /// otherwise.
- ///
- public bool IsDirty
- {
- get => _historyText.IsDirty (_model.GetAllLines ());
- set => _historyText.Clear (_model.GetAllLines ());
- }
-
- /// Gets or sets the left column.
- public int LeftColumn
- {
- get => _leftColumn;
- set
- {
- if (value > 0 && _wordWrap)
- {
- return;
- }
-
- _leftColumn = Math.Max (Math.Min (value, Maxlength - 1), 0);
- }
- }
-
- /// Gets the number of lines.
- public int Lines => _model.Count;
-
- /// Gets the maximum visible length line.
- public int Maxlength => _model.GetMaxVisibleLine (_topRow, _topRow + Viewport.Height, TabWidth);
-
- /// Gets or sets a value indicating whether this is a multiline text view.
- public bool Multiline
- {
- get => _multiline;
- set
- {
- _multiline = value;
-
- if (_multiline && !_allowsTab)
- {
- AllowsTab = true;
- }
-
- if (_multiline && !_allowsReturn)
- {
- AllowsReturn = true;
- }
-
- if (!_multiline)
- {
- AllowsReturn = false;
- AllowsTab = false;
- WordWrap = false;
- CurrentColumn = 0;
- CurrentRow = 0;
- _savedHeight = Height;
-
- Height = Dim.Auto (DimAutoStyle.Text, 1);
-
- if (!IsInitialized)
- {
- _model.LoadString (Text);
- }
-
- SetNeedsDraw ();
- }
- else if (_multiline && _savedHeight is { })
- {
- Height = _savedHeight;
- SetNeedsDraw ();
- }
-
- KeyBindings.Remove (Key.Enter);
- KeyBindings.Add (Key.Enter, Multiline ? Command.NewLine : Command.Accept);
- }
- }
-
- /// Gets or sets whether the is in read-only mode or not
- /// Boolean value(Default false)
- public bool ReadOnly
- {
- get => _isReadOnly;
- set
- {
- if (value != _isReadOnly)
- {
- _isReadOnly = value;
-
- SetNeedsDraw ();
- WrapTextModel ();
- Adjust ();
- }
- }
- }
-
- /// Length of the selected text.
- public int SelectedLength => GetSelectedLength ();
-
- ///
- /// Gets the selected text as
- ///
- /// List{List{Cell}}
- ///
- ///
- public List> SelectedCellsList
- {
- get
- {
- GetRegion (out List> selectedCellsList);
-
- return selectedCellsList;
- }
- }
-
- /// The selected text.
- public string SelectedText
- {
- get
- {
- if (!IsSelecting || (_model.Count == 1 && _model.GetLine (0).Count == 0))
- {
- return string.Empty;
- }
-
- return GetSelectedRegion ();
- }
- }
-
- /// Get or sets whether the user is currently selecting text.
- public bool IsSelecting { get; set; }
-
- /// Start column position of the selected text.
- public int SelectionStartColumn
- {
- get => _selectionStartColumn;
- set
- {
- List line = _model.GetLine (_selectionStartRow);
-
- _selectionStartColumn = value < 0 ? 0 :
- value > line.Count ? line.Count : value;
- IsSelecting = true;
- SetNeedsDraw ();
- Adjust ();
- }
- }
-
- /// Start row position of the selected text.
- public int SelectionStartRow
- {
- get => _selectionStartRow;
- set
- {
- _selectionStartRow = value < 0 ? 0 :
- value > _model.Count - 1 ? Math.Max (_model.Count - 1, 0) : value;
- IsSelecting = true;
- SetNeedsDraw ();
- Adjust ();
- }
- }
-
- /// Gets or sets a value indicating the number of whitespace when pressing the TAB key.
- public int TabWidth
- {
- get => _tabWidth;
- set
- {
- _tabWidth = Math.Max (value, 0);
-
- if (_tabWidth > 0 && !AllowsTab)
- {
- AllowsTab = true;
- }
-
- SetNeedsDraw ();
- }
- }
-
- /// Sets or gets the text in the .
- ///
- /// The event is fired whenever this property is set. Note, however, that Text is not
- /// set by as the user types.
- ///
- public override string Text
- {
- get
- {
- if (_wordWrap)
- {
- return _wrapManager!.Model.ToString ();
- }
-
- return _model.ToString ();
- }
- set
- {
- ResetPosition ();
- _model.LoadString (value);
-
- if (_wordWrap)
- {
- _wrapManager = new (_model);
- _model = _wrapManager.WrapModel (Viewport.Width, out _, out _, out _, out _);
- }
-
- OnTextChanged ();
- SetNeedsDraw ();
-
- _historyText.Clear (_model.GetAllLines ());
- }
- }
-
- /// Gets or sets the top row.
- public int TopRow
- {
- get => _topRow;
- set => _topRow = Math.Max (Math.Min (value, Lines - 1), 0);
- }
-
- ///
- /// Tracks whether the text view should be considered "used", that is, that the user has moved in the entry, so
- /// new input should be appended at the cursor position, rather than clearing the entry
- ///
- public bool Used { get; set; }
-
- /// Allows word wrap the to fit the available container width.
- public bool WordWrap
- {
- get => _wordWrap;
- set
- {
- if (value == _wordWrap)
- {
- return;
- }
-
- if (value && !_multiline)
- {
- return;
- }
-
- _wordWrap = value;
- ResetPosition ();
-
- if (_wordWrap)
- {
- _wrapManager = new (_model);
- WrapTextModel ();
- }
- else if (!_wordWrap && _wrapManager is { })
- {
- _model = _wrapManager.Model;
- }
-
- SetNeedsDraw ();
- }
- }
-
- ///
- /// Gets or sets whether the word forward and word backward navigation should use the same or equivalent rune type.
- /// Default is false meaning using equivalent rune type.
- ///
- public bool UseSameRuneTypeForWords { get; set; }
-
- ///
- /// 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 false meaning that the spaces at right are included in the selection.
- ///
- public bool SelectWordOnlyOnDoubleClick { get; set; }
-
- /// Allows clearing the items updating the original text.
- public void ClearHistoryChanges () { _historyText?.Clear (_model.GetAllLines ()); }
-
- /// Closes the contents of the stream into the .
- /// true, if stream was closed, false otherwise.
- public bool CloseFile ()
- {
- SetWrapModel ();
- bool res = _model.CloseFile ();
- ResetPosition ();
- SetNeedsDraw ();
- UpdateWrapModel ();
-
- return res;
- }
-
- /// Raised when the contents of the are changed.
- ///
- /// Unlike the event, this event is raised whenever the user types or otherwise changes
- /// the contents of the .
- ///
- public event EventHandler? ContentsChanged;
-
- internal void ApplyCellsAttribute (Attribute attribute)
- {
- if (!ReadOnly && SelectedLength > 0)
- {
- int startRow = Math.Min (SelectionStartRow, CurrentRow);
- int endRow = Math.Max (CurrentRow, SelectionStartRow);
- int startCol = SelectionStartRow <= CurrentRow ? SelectionStartColumn : CurrentColumn;
- int endCol = CurrentRow >= SelectionStartRow ? CurrentColumn : SelectionStartColumn;
- List> selectedCellsOriginal = [];
- List> selectedCellsChanged = [];
-
- for (int r = startRow; r <= endRow; r++)
- {
- List line = GetLine (r);
-
- selectedCellsOriginal.Add ([.. line]);
-
- for (int c = r == startRow ? startCol : 0;
- c < (r == endRow ? endCol : line.Count);
- c++)
- {
- Cell cell = line [c]; // Copy value to a new variable
- cell.Attribute = attribute; // Modify the copy
- line [c] = cell; // Assign the modified copy back
- }
-
- selectedCellsChanged.Add ([.. GetLine (r)]);
- }
-
- GetSelectedRegion ();
- IsSelecting = false;
-
- _historyText.Add (
- [.. selectedCellsOriginal],
- new Point (startCol, startRow)
- );
-
- _historyText.Add (
- [.. selectedCellsChanged],
- new Point (startCol, startRow),
- TextEditingLineStatus.Attribute
- );
- }
- }
-
- private Attribute? GetSelectedCellAttribute ()
- {
- List line;
-
- if (SelectedLength > 0)
- {
- line = GetLine (SelectionStartRow);
-
- if (line [Math.Min (SelectionStartColumn, line.Count - 1)].Attribute is { } attributeSel)
- {
- return new (attributeSel);
- }
-
- return GetAttributeForRole (VisualRole.Active);
- }
-
- line = GetCurrentLine ();
-
- if (line [Math.Min (CurrentColumn, line.Count - 1)].Attribute is { } attribute)
- {
- return new (attribute);
- }
-
- return GetAttributeForRole (VisualRole.Active);
- }
-
- ///
- /// Open a dialog to set the foreground and background colors.
- ///
- public void PromptForColors ()
- {
- if (!ColorPicker.Prompt (
- "Colors",
- GetSelectedCellAttribute (),
- out Attribute newAttribute
- ))
- {
- return;
- }
-
- var attribute = new Attribute (
- newAttribute.Foreground,
- newAttribute.Background,
- newAttribute.Style
- );
-
- ApplyCellsAttribute (attribute);
- }
-
- private string? _copiedText;
- private List> _copiedCellsList = [];
-
- /// Copy the selected text to the clipboard contents.
- public void Copy ()
- {
- SetWrapModel ();
-
- if (IsSelecting)
- {
- _copiedText = GetRegion (out _copiedCellsList);
- SetClipboard (_copiedText);
- _copyWithoutSelection = false;
- }
- else
- {
- List currentLine = GetCurrentLine ();
- _copiedCellsList.Add (currentLine);
- _copiedText = Cell.ToString (currentLine);
- SetClipboard (_copiedText);
- _copyWithoutSelection = true;
- }
-
- UpdateWrapModel ();
- DoNeededAction ();
- }
-
- /// Cut the selected text to the clipboard contents.
- public void Cut ()
- {
- SetWrapModel ();
- _copiedText = GetRegion (out _copiedCellsList);
- SetClipboard (_copiedText);
-
- if (!_isReadOnly)
- {
- ClearRegion ();
-
- _historyText.Add (
- [new (GetCurrentLine ())],
- CursorPosition,
- TextEditingLineStatus.Replaced
- );
- }
-
- UpdateWrapModel ();
- IsSelecting = false;
- DoNeededAction ();
- OnContentsChanged ();
- }
-
- /// Deletes all text.
- public void DeleteAll ()
- {
- if (Lines == 0)
- {
- return;
- }
-
- _selectionStartColumn = 0;
- _selectionStartRow = 0;
- MoveBottomEndExtend ();
- DeleteCharLeft ();
- SetNeedsDraw ();
- }
-
- /// Deletes all the selected or a single character at left from the position of the cursor.
- public void DeleteCharLeft ()
- {
- if (_isReadOnly)
- {
- return;
- }
-
- SetWrapModel ();
-
- if (IsSelecting)
- {
- _historyText.Add (new () { new (GetCurrentLine ()) }, CursorPosition);
-
- ClearSelectedRegion ();
-
- List currentLine = GetCurrentLine ();
-
- _historyText.Add (
- new () { new (currentLine) },
- CursorPosition,
- TextEditingLineStatus.Replaced
- );
-
- UpdateWrapModel ();
- OnContentsChanged ();
-
- return;
- }
-
- if (DeleteTextBackwards ())
- {
- UpdateWrapModel ();
- OnContentsChanged ();
-
- return;
- }
-
- UpdateWrapModel ();
-
- DoNeededAction ();
- OnContentsChanged ();
- }
-
- /// Deletes all the selected or a single character at right from the position of the cursor.
- public void DeleteCharRight ()
- {
- if (_isReadOnly)
- {
- return;
- }
-
- SetWrapModel ();
-
- if (IsSelecting)
- {
- _historyText.Add (new () { new (GetCurrentLine ()) }, CursorPosition);
-
- ClearSelectedRegion ();
-
- List currentLine = GetCurrentLine ();
-
- _historyText.Add (
- new () { new (currentLine) },
- CursorPosition,
- TextEditingLineStatus.Replaced
- );
-
- UpdateWrapModel ();
- OnContentsChanged ();
-
- return;
- }
-
- if (DeleteTextForwards ())
- {
- UpdateWrapModel ();
- OnContentsChanged ();
-
- return;
- }
-
- UpdateWrapModel ();
-
- DoNeededAction ();
- OnContentsChanged ();
- }
-
- /// Invoked when the normal color is drawn.
- public event EventHandler? DrawNormalColor;
-
- /// Invoked when the ready only color is drawn.
- public event EventHandler? DrawReadOnlyColor;
-
- /// Invoked when the selection color is drawn.
- public event EventHandler? DrawSelectionColor;
-
- ///
- /// Invoked when the used color is drawn. The Used Color is used to indicate if the
- /// was pressed and enabled.
- ///
- public event EventHandler? DrawUsedColor;
-
- /// Find the next text based on the match case with the option to replace it.
- /// The text to find.
- /// trueIf all the text was forward searched.falseotherwise.
- /// The match case setting.
- /// The match whole word setting.
- /// The text to replace.
- /// trueIf is replacing.falseotherwise.
- /// trueIf the text was found.falseotherwise.
- public bool FindNextText (
- string textToFind,
- out bool gaveFullTurn,
- bool matchCase = false,
- bool matchWholeWord = false,
- string? textToReplace = null,
- bool replace = false
- )
- {
- if (_model.Count == 0)
- {
- gaveFullTurn = false;
-
- return false;
- }
-
- SetWrapModel ();
- ResetContinuousFind ();
-
- (Point current, bool found) foundPos =
- _model.FindNextText (textToFind, out gaveFullTurn, matchCase, matchWholeWord);
-
- return SetFoundText (textToFind, foundPos, textToReplace, replace);
- }
-
- /// Find the previous text based on the match case with the option to replace it.
- /// The text to find.
- /// trueIf all the text was backward searched.falseotherwise.
- /// The match case setting.
- /// The match whole word setting.
- /// The text to replace.
- /// trueIf the text was found.falseotherwise.
- /// trueIf the text was found.falseotherwise.
- public bool FindPreviousText (
- string textToFind,
- out bool gaveFullTurn,
- bool matchCase = false,
- bool matchWholeWord = false,
- string? textToReplace = null,
- bool replace = false
- )
- {
- if (_model.Count == 0)
- {
- gaveFullTurn = false;
-
- return false;
- }
-
- SetWrapModel ();
- ResetContinuousFind ();
-
- (Point current, bool found) foundPos =
- _model.FindPreviousText (textToFind, out gaveFullTurn, matchCase, matchWholeWord);
-
- return SetFoundText (textToFind, foundPos, textToReplace, replace);
- }
-
- /// Reset the flag to stop continuous find.
- public void FindTextChanged () { _continuousFind = false; }
-
- /// Gets all lines of characters.
- ///
- public List> GetAllLines () { return _model.GetAllLines (); }
-
- ///
- /// Returns the characters on the current line (where the cursor is positioned). Use
- /// to determine the position of the cursor within that line
- ///
- ///
- public List GetCurrentLine () { return _model.GetLine (CurrentRow); }
-
- /// Returns the characters on the .
- /// The intended line.
- ///
- public List GetLine (int line) { return _model.GetLine (line); }
-
- ///
- protected override bool OnGettingAttributeForRole (in VisualRole role, ref Attribute currentAttribute)
- {
- if (role == VisualRole.Normal)
- {
- currentAttribute = GetAttributeForRole (VisualRole.Editable);
-
- return true;
- }
-
- return base.OnGettingAttributeForRole (role, ref currentAttribute);
- }
-
- ///
- /// Inserts the given text at the current cursor position exactly as if the user had just
- /// typed it
- ///
- /// Text to add
- public void InsertText (string toAdd)
- {
- foreach (char ch in toAdd)
- {
- Key key;
-
- try
- {
- key = new (ch);
- }
- catch (Exception)
- {
- throw new ArgumentException (
- $"Cannot insert character '{ch}' because it does not map to a Key"
- );
- }
-
- InsertText (key);
-
- if (NeedsDraw)
- {
- Adjust ();
- }
- else
- {
- PositionCursor ();
- }
- }
- }
-
- /// Loads the contents of the file into the .
- /// true, if file was loaded, false otherwise.
- /// Path to the file to load.
- public bool Load (string path)
- {
- SetWrapModel ();
- bool res;
-
- try
- {
- SetWrapModel ();
- res = _model.LoadFile (path);
- _historyText.Clear (_model.GetAllLines ());
- ResetPosition ();
- }
- finally
- {
- UpdateWrapModel ();
- SetNeedsDraw ();
- Adjust ();
- }
-
- UpdateWrapModel ();
-
- return res;
- }
-
- /// Loads the contents of the stream into the .
- /// true, if stream was loaded, false otherwise.
- /// Stream to load the contents from.
- public void Load (Stream stream)
- {
- SetWrapModel ();
- _model.LoadStream (stream);
- _historyText.Clear (_model.GetAllLines ());
- ResetPosition ();
- SetNeedsDraw ();
- UpdateWrapModel ();
- }
-
- /// Loads the contents of the list into the .
- /// Text cells list to load the contents from.
- public void Load (List cells)
- {
- SetWrapModel ();
- _model.LoadCells (cells, GetAttributeForRole (VisualRole.Focus));
- _historyText.Clear (_model.GetAllLines ());
- ResetPosition ();
- SetNeedsDraw ();
- UpdateWrapModel ();
- InheritsPreviousAttribute = true;
- }
-
- /// Loads the contents of the list of list into the .
- /// List of rune cells list to load the contents from.
- public void Load (List> cellsList)
- {
- SetWrapModel ();
- InheritsPreviousAttribute = true;
- _model.LoadListCells (cellsList, GetAttributeForRole (VisualRole.Focus));
- _historyText.Clear (_model.GetAllLines ());
- ResetPosition ();
- SetNeedsDraw ();
- UpdateWrapModel ();
- }
-
- ///
- protected override bool OnMouseEvent (MouseEventArgs ev)
- {
- if (ev is { IsSingleDoubleOrTripleClicked: false, IsPressed: false, IsReleased: false, IsWheel: false }
- && !ev.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition)
- && !ev.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ButtonShift)
- && !ev.Flags.HasFlag (MouseFlags.Button1DoubleClicked | MouseFlags.ButtonShift)
- && !ev.Flags.HasFlag (ContextMenu!.MouseFlags))
- {
- return false;
- }
-
- if (!CanFocus)
- {
- return true;
- }
-
- if (!HasFocus)
- {
- SetFocus ();
- }
-
- _continuousFind = false;
-
- // Give autocomplete first opportunity to respond to mouse clicks
- if (SelectedLength == 0 && Autocomplete.OnMouseEvent (ev, true))
- {
- return true;
- }
-
- if (ev.Flags == MouseFlags.Button1Clicked)
- {
- if (_isButtonReleased)
- {
- _isButtonReleased = false;
-
- if (SelectedLength == 0)
- {
- StopSelecting ();
- }
-
- return true;
- }
-
- if (_shiftSelecting && !_isButtonShift)
- {
- StopSelecting ();
- }
-
- ProcessMouseClick (ev, out _);
-
- if (Used)
- {
- PositionCursor ();
- }
- else
- {
- SetNeedsDraw ();
- }
-
- _lastWasKill = false;
- _columnTrack = CurrentColumn;
- }
- else if (ev.Flags == MouseFlags.WheeledDown)
- {
- _lastWasKill = false;
- _columnTrack = CurrentColumn;
- ScrollTo (_topRow + 1);
- }
- else if (ev.Flags == MouseFlags.WheeledUp)
- {
- _lastWasKill = false;
- _columnTrack = CurrentColumn;
- ScrollTo (_topRow - 1);
- }
- else if (ev.Flags == MouseFlags.WheeledRight)
- {
- _lastWasKill = false;
- _columnTrack = CurrentColumn;
- ScrollTo (_leftColumn + 1, false);
- }
- else if (ev.Flags == MouseFlags.WheeledLeft)
- {
- _lastWasKill = false;
- _columnTrack = CurrentColumn;
- ScrollTo (_leftColumn - 1, false);
- }
- else if (ev.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition))
- {
- ProcessMouseClick (ev, out List line);
- PositionCursor ();
-
- if (_model.Count > 0 && _shiftSelecting && IsSelecting)
- {
- if (CurrentRow - _topRow >= Viewport.Height - 1 && _model.Count > _topRow + CurrentRow)
- {
- ScrollTo (_topRow + Viewport.Height);
- }
- else if (_topRow > 0 && CurrentRow <= _topRow)
- {
- ScrollTo (_topRow - Viewport.Height);
- }
- else if (ev.Position.Y >= Viewport.Height)
- {
- ScrollTo (_model.Count);
- }
- else if (ev.Position.Y < 0 && _topRow > 0)
- {
- ScrollTo (0);
- }
-
- if (CurrentColumn - _leftColumn >= Viewport.Width - 1 && line.Count > _leftColumn + CurrentColumn)
- {
- ScrollTo (_leftColumn + Viewport.Width, false);
- }
- else if (_leftColumn > 0 && CurrentColumn <= _leftColumn)
- {
- ScrollTo (_leftColumn - Viewport.Width, false);
- }
- else if (ev.Position.X >= Viewport.Width)
- {
- ScrollTo (line.Count, false);
- }
- else if (ev.Position.X < 0 && _leftColumn > 0)
- {
- ScrollTo (0, false);
- }
- }
-
- _lastWasKill = false;
- _columnTrack = CurrentColumn;
- }
- else if (ev.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ButtonShift))
- {
- if (!_shiftSelecting)
- {
- _isButtonShift = true;
- StartSelecting ();
- }
-
- ProcessMouseClick (ev, out _);
- PositionCursor ();
- _lastWasKill = false;
- _columnTrack = CurrentColumn;
- }
- else if (ev.Flags.HasFlag (MouseFlags.Button1Pressed))
- {
- if (_shiftSelecting)
- {
- _clickWithSelecting = true;
- StopSelecting ();
- }
-
- ProcessMouseClick (ev, out _);
- PositionCursor ();
-
- if (!IsSelecting)
- {
- StartSelecting ();
- }
-
- _lastWasKill = false;
- _columnTrack = CurrentColumn;
-
- if (App?.Mouse.MouseGrabView is null)
- {
- App?.Mouse.GrabMouse (this);
- }
- }
- else if (ev.Flags.HasFlag (MouseFlags.Button1Released))
- {
- _isButtonReleased = true;
- App?.Mouse.UngrabMouse ();
- }
- else if (ev.Flags.HasFlag (MouseFlags.Button1DoubleClicked))
- {
- if (ev.Flags.HasFlag (MouseFlags.ButtonShift))
- {
- if (!IsSelecting)
- {
- StartSelecting ();
- }
- }
- else if (IsSelecting)
- {
- StopSelecting ();
- }
-
- ProcessMouseClick (ev, out List line);
-
- if (!IsSelecting)
- {
- StartSelecting ();
- }
-
- (int startCol, int col, int row)? newPos = _model.ProcessDoubleClickSelection (SelectionStartColumn, CurrentColumn, CurrentRow, UseSameRuneTypeForWords, SelectWordOnlyOnDoubleClick);
-
- if (newPos.HasValue)
- {
- SelectionStartColumn = newPos.Value.startCol;
- CurrentColumn = newPos.Value.col;
- CurrentRow = newPos.Value.row;
- }
-
- PositionCursor ();
- _lastWasKill = false;
- _columnTrack = CurrentColumn;
- SetNeedsDraw ();
- }
- else if (ev.Flags.HasFlag (MouseFlags.Button1TripleClicked))
- {
- if (IsSelecting)
- {
- StopSelecting ();
- }
-
- ProcessMouseClick (ev, out List line);
- CurrentColumn = 0;
-
- if (!IsSelecting)
- {
- StartSelecting ();
- }
-
- CurrentColumn = line.Count;
- PositionCursor ();
- _lastWasKill = false;
- _columnTrack = CurrentColumn;
- SetNeedsDraw ();
- }
- else if (ev.Flags == ContextMenu!.MouseFlags)
- {
- ShowContextMenu (ev.ScreenPosition);
- }
-
- OnUnwrappedCursorPosition ();
-
- return true;
- }
-
- /// Will scroll the to the last line and position the cursor there.
- public void MoveEnd ()
- {
- CurrentRow = _model.Count - 1;
- List line = GetCurrentLine ();
- CurrentColumn = line.Count;
- TrackColumn ();
- DoNeededAction ();
- }
-
- /// Will scroll the to the first line and position the cursor there.
- public void MoveHome ()
- {
- CurrentRow = 0;
- _topRow = 0;
- CurrentColumn = 0;
- _leftColumn = 0;
- TrackColumn ();
- DoNeededAction ();
- }
-
- ///
- /// Called when the contents of the TextView change. E.g. when the user types text or deletes text. Raises the
- /// event.
- ///
- public virtual void OnContentsChanged ()
- {
- ContentsChanged?.Invoke (this, new (CurrentRow, CurrentColumn));
-
- ProcessInheritsPreviousScheme (CurrentRow, CurrentColumn);
- ProcessAutocomplete ();
- }
-
- ///
- protected override bool OnDrawingContent ()
- {
- _isDrawing = true;
-
- SetAttributeForRole (Enabled ? VisualRole.Editable : VisualRole.Disabled);
-
- (int width, int height) offB = OffSetBackground ();
- int right = Viewport.Width + offB.width;
- int bottom = Viewport.Height + offB.height;
- var row = 0;
-
- for (int idxRow = _topRow; idxRow < _model.Count; idxRow++)
- {
- List line = _model.GetLine (idxRow);
- int lineRuneCount = line.Count;
- var col = 0;
-
- Move (0, row);
-
- for (int idxCol = _leftColumn; idxCol < lineRuneCount; idxCol++)
- {
- string text = idxCol >= lineRuneCount ? " " : line [idxCol].Grapheme;
- int cols = text.GetColumns (false);
-
- if (idxCol < line.Count && IsSelecting && PointInSelection (idxCol, idxRow))
- {
- OnDrawSelectionColor (line, idxCol, idxRow);
- }
- else if (idxCol == CurrentColumn && idxRow == CurrentRow && !IsSelecting && !Used && HasFocus && idxCol < lineRuneCount)
- {
- OnDrawUsedColor (line, idxCol, idxRow);
- }
- else if (ReadOnly)
- {
- OnDrawReadOnlyColor (line, idxCol, idxRow);
- }
- else
- {
- OnDrawNormalColor (line, idxCol, idxRow);
- }
-
- if (text == "\t")
- {
- cols += TabWidth + 1;
-
- if (col + cols > right)
- {
- cols = right - col;
- }
-
- for (var i = 0; i < cols; i++)
- {
- if (col + i < right)
- {
- AddRune (col + i, row, (Rune)' ');
- }
- }
- }
- else
- {
- AddStr (col, row, text);
-
- // Ensures that cols less than 0 to be 1 because it will be converted to a printable rune
- cols = Math.Max (cols, 1);
- }
-
- if (!TextModel.SetCol (ref col, Viewport.Right, cols))
- {
- break;
- }
-
- if (idxCol + 1 < lineRuneCount && col + line [idxCol + 1].Grapheme.GetColumns () > right)
- {
- break;
- }
- }
-
- if (col < right)
- {
- SetAttributeForRole (ReadOnly ? VisualRole.ReadOnly : VisualRole.Editable);
- ClearRegion (col, row, right, row + 1);
- }
-
- row++;
- }
-
- if (row < bottom)
- {
- SetAttributeForRole (ReadOnly ? VisualRole.ReadOnly : VisualRole.Editable);
- ClearRegion (Viewport.Left, row, right, bottom);
- }
-
- _isDrawing = false;
-
- return false;
- }
-
- ///
- protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? view)
- {
- if (App?.Mouse.MouseGrabView is { } && App?.Mouse.MouseGrabView == this)
- {
- App?.Mouse.UngrabMouse ();
- }
- }
-
- ///
- protected override bool OnKeyDown (Key key)
- {
- if (!key.IsValid)
- {
- return false;
- }
-
- // Give autocomplete first opportunity to respond to key presses
- if (SelectedLength == 0 && Autocomplete.Suggestions.Count > 0 && Autocomplete.ProcessKey (key))
- {
- return true;
- }
-
- return false;
- }
-
- ///
- protected override bool OnKeyDownNotHandled (Key a)
- {
- if (!CanFocus)
- {
- return true;
- }
-
- ResetColumnTrack ();
-
- // Ignore control characters and other special keys
- if (!a.IsKeyCodeAtoZ && (a.KeyCode < KeyCode.Space || a.KeyCode > KeyCode.CharMask))
- {
- return false;
- }
-
- InsertText (a);
- DoNeededAction ();
-
- return true;
- }
-
- ///
- public override bool OnKeyUp (Key key)
- {
- if (key == Key.Space.WithCtrl)
- {
- return true;
- }
-
- return false;
- }
-
- /// Invoke the event with the unwrapped .
- public virtual void OnUnwrappedCursorPosition (int? cRow = null, int? cCol = null)
- {
- int? row = cRow ?? CurrentRow;
- int? col = cCol ?? CurrentColumn;
-
- if (cRow is null && cCol is null && _wordWrap)
- {
- row = _wrapManager!.GetModelLineFromWrappedLines (CurrentRow);
- col = _wrapManager.GetModelColFromWrappedLines (CurrentRow, CurrentColumn);
- }
-
- UnwrappedCursorPosition?.Invoke (this, new (col.Value, row.Value));
- }
-
- /// Paste the clipboard contents into the current selected position.
- public void Paste ()
- {
- if (_isReadOnly)
- {
- return;
- }
-
- SetWrapModel ();
- string? contents = Clipboard.Contents;
-
- if (_copyWithoutSelection && contents!.FirstOrDefault (x => x is '\n' or '\r') == 0)
- {
- List runeList = contents is null ? [] : Cell.ToCellList (contents);
- List currentLine = GetCurrentLine ();
-
- _historyText.Add ([new (currentLine)], CursorPosition);
-
- List> addedLine = [new (currentLine), runeList];
-
- _historyText.Add (
- [.. addedLine],
- CursorPosition,
- TextEditingLineStatus.Added
- );
-
- _model.AddLine (CurrentRow, runeList);
- CurrentRow++;
-
- _historyText.Add (
- [new (GetCurrentLine ())],
- CursorPosition,
- TextEditingLineStatus.Replaced
- );
-
- SetNeedsDraw ();
- OnContentsChanged ();
- }
- else
- {
- if (IsSelecting)
- {
- ClearRegion ();
- }
-
- _copyWithoutSelection = false;
- InsertAllText (contents!, true);
-
- if (IsSelecting)
- {
- _historyText.ReplaceLast (
- [new (GetCurrentLine ())],
- CursorPosition,
- TextEditingLineStatus.Original
- );
- }
-
- SetNeedsDraw ();
- }
-
- UpdateWrapModel ();
- IsSelecting = false;
- DoNeededAction ();
- }
-
- /// Positions the cursor on the current row and column
- public override Point? PositionCursor ()
- {
- ProcessAutocomplete ();
-
- if (!CanFocus || !Enabled || Driver is null)
- {
- return null;
- }
-
- if (App?.Mouse.MouseGrabView == this && IsSelecting)
- {
- // BUGBUG: customized rect aren't supported now because the Redraw isn't using the Intersect method.
- //var minRow = Math.Min (Math.Max (Math.Min (selectionStartRow, currentRow) - topRow, 0), Viewport.Height);
- //var maxRow = Math.Min (Math.Max (Math.Max (selectionStartRow, currentRow) - topRow, 0), Viewport.Height);
- //SetNeedsDraw (new (0, minRow, Viewport.Width, maxRow));
- SetNeedsDraw ();
- }
-
- List line = _model.GetLine (CurrentRow);
- var col = 0;
-
- if (line.Count > 0)
- {
- for (int idx = _leftColumn; idx < line.Count; idx++)
- {
- if (idx >= CurrentColumn)
- {
- break;
- }
-
- int cols = line [idx].Grapheme.GetColumns ();
-
- if (line [idx].Grapheme == "\t")
- {
- cols += TabWidth + 1;
- }
- else
- {
- // Ensures that cols less than 0 to be 1 because it will be converted to a printable rune
- cols = Math.Max (cols, 1);
- }
-
- if (!TextModel.SetCol (ref col, Viewport.Width, cols))
- {
- col = CurrentColumn;
-
- break;
- }
- }
- }
-
- int posX = CurrentColumn - _leftColumn;
- int posY = CurrentRow - _topRow;
-
- if (posX > -1 && col >= posX && posX < Viewport.Width && _topRow <= CurrentRow && posY < Viewport.Height)
- {
- Move (col, CurrentRow - _topRow);
-
- return new (col, CurrentRow - _topRow);
- }
-
- return null; // Hide cursor
- }
-
- /// Redoes the latest changes.
- public void Redo ()
- {
- if (ReadOnly)
- {
- return;
- }
-
- _historyText.Redo ();
- }
-
- /// Replaces all the text based on the match case.
- /// The text to find.
- /// The match case setting.
- /// The match whole word setting.
- /// The text to replace.
- /// trueIf the text was found.falseotherwise.
- public bool ReplaceAllText (
- string textToFind,
- bool matchCase = false,
- bool matchWholeWord = false,
- string? textToReplace = null
- )
- {
- if (_isReadOnly || _model.Count == 0)
- {
- return false;
- }
-
- SetWrapModel ();
- ResetContinuousFind ();
-
- (Point current, bool found) foundPos =
- _model.ReplaceAllText (textToFind, matchCase, matchWholeWord, textToReplace);
-
- return SetFoundText (textToFind, foundPos, textToReplace, false, true);
- }
-
- ///
- /// Will scroll the to display the specified row at the top if is
- /// true or will scroll the to display the specified column at the left if
- /// is false.
- ///
- ///
- /// Row that should be displayed at the top or Column that should be displayed at the left, if the value
- /// is negative it will be reset to zero
- ///
- /// If true (default) the is a row, column otherwise.
- public void ScrollTo (int idx, bool isRow = true)
- {
- if (idx < 0)
- {
- idx = 0;
- }
-
- if (isRow)
- {
- _topRow = Math.Max (idx > _model.Count - 1 ? _model.Count - 1 : idx, 0);
- }
- else if (!_wordWrap)
- {
- int maxlength =
- _model.GetMaxVisibleLine (_topRow, _topRow + Viewport.Height, TabWidth);
- _leftColumn = Math.Max (!_wordWrap && idx > maxlength - 1 ? maxlength - 1 : idx, 0);
- }
-
- SetNeedsDraw ();
- }
-
- /// Select all text.
- public void SelectAll ()
- {
- if (_model.Count == 0)
- {
- return;
- }
-
- StartSelecting ();
- _selectionStartColumn = 0;
- _selectionStartRow = 0;
- CurrentColumn = _model.GetLine (_model.Count - 1).Count;
- CurrentRow = _model.Count - 1;
- SetNeedsDraw ();
- }
-
- ///// Raised when the property of the changes.
- /////
- ///// The property of only changes when it is explicitly set, not as the
- ///// user types. To be notified as the user changes the contents of the TextView see .
- /////
- //public event EventHandler? TextChanged;
-
- /// Undoes the latest changes.
- public void Undo ()
- {
- if (ReadOnly)
- {
- return;
- }
-
- _historyText.Undo ();
- }
-
- /// Invoked with the unwrapped .
- public event EventHandler? UnwrappedCursorPosition;
-
- ///
- /// Sets the to an appropriate color for rendering the given
- /// of the current . Override to provide custom coloring by calling
- /// Defaults to .
- ///
- /// The line.
- /// The col index.
- /// The row index.
- protected virtual void OnDrawNormalColor (List line, int idxCol, int idxRow)
- {
- (int Row, int Col) unwrappedPos = GetUnwrappedPosition (idxRow, idxCol);
- var ev = new CellEventArgs (line, idxCol, unwrappedPos);
- DrawNormalColor?.Invoke (this, ev);
-
- if (line [idxCol].Attribute is { })
- {
- Attribute? attribute = line [idxCol].Attribute;
- SetAttribute ((Attribute)attribute!);
- }
- else
- {
- SetAttribute (GetAttributeForRole (VisualRole.Normal));
- }
- }
-
- ///
- /// Sets the to an appropriate color for rendering the given
- /// of the current . Override to provide custom coloring by calling
- /// Defaults to .
- ///
- /// The line.
- /// The col index.
- /// ///
- /// The row index.
- protected virtual void OnDrawReadOnlyColor (List line, int idxCol, int idxRow)
- {
- (int Row, int Col) unwrappedPos = GetUnwrappedPosition (idxRow, idxCol);
- var ev = new CellEventArgs (line, idxCol, unwrappedPos);
- DrawReadOnlyColor?.Invoke (this, ev);
-
- Attribute? cellAttribute = line [idxCol].Attribute is { } ? line [idxCol].Attribute : GetAttributeForRole (VisualRole.ReadOnly);
-
- if (cellAttribute!.Value.Foreground == cellAttribute.Value.Background)
- {
- SetAttribute (new (cellAttribute.Value.Foreground, cellAttribute.Value.Background, cellAttribute.Value.Style));
- }
- else
- {
- SetAttributeForRole (VisualRole.ReadOnly);
- }
- }
-
- ///
- /// Sets the to an appropriate color for rendering the given
- /// of the current . Override to provide custom coloring by calling
- /// Defaults to .
- ///
- /// The line.
- /// The col index.
- /// ///
- /// The row index.
- protected virtual void OnDrawSelectionColor (List line, int idxCol, int idxRow)
- {
- (int Row, int Col) unwrappedPos = GetUnwrappedPosition (idxRow, idxCol);
- var ev = new CellEventArgs (line, idxCol, unwrappedPos);
- DrawSelectionColor?.Invoke (this, ev);
-
- if (line [idxCol].Attribute is { })
- {
- Attribute? attribute = line [idxCol].Attribute;
- Attribute? active = GetAttributeForRole (VisualRole.Active);
- SetAttribute (new (active!.Value.Foreground, active.Value.Background, attribute!.Value.Style));
- }
- else
- {
- SetAttributeForRole (VisualRole.Active);
- }
- }
-
- ///
- /// Sets the to an appropriate color for rendering the given
- /// of the current . Override to provide custom coloring by calling
- /// Defaults to .
- ///
- /// The line.
- /// The col index.
- /// ///
- /// The row index.
- protected virtual void OnDrawUsedColor (List line, int idxCol, int idxRow)
- {
- (int Row, int Col) unwrappedPos = GetUnwrappedPosition (idxRow, idxCol);
- var ev = new CellEventArgs (line, idxCol, unwrappedPos);
- DrawUsedColor?.Invoke (this, ev);
-
- if (line [idxCol].Attribute is { })
- {
- Attribute? attribute = line [idxCol].Attribute;
- SetValidUsedColor (attribute!);
- }
- else
- {
- SetValidUsedColor (GetAttributeForRole (VisualRole.Focus));
- }
- }
-
- private void Adjust ()
- {
- (int width, int height) offB = OffSetBackground ();
- List line = GetCurrentLine ();
- bool need = NeedsDraw || _wrapNeeded || !Used;
- (int size, int length) tSize = TextModel.DisplaySize (line, -1, -1, false, TabWidth);
- (int size, int length) dSize = TextModel.DisplaySize (line, _leftColumn, CurrentColumn, true, TabWidth);
-
- if (!_wordWrap && CurrentColumn < _leftColumn)
- {
- _leftColumn = CurrentColumn;
- need = true;
- }
- else if (!_wordWrap
- && (CurrentColumn - _leftColumn + 1 > Viewport.Width + offB.width || dSize.size + 1 >= Viewport.Width + offB.width))
- {
- _leftColumn = TextModel.CalculateLeftColumn (
- line,
- _leftColumn,
- CurrentColumn,
- Viewport.Width + offB.width,
- TabWidth
- );
- need = true;
- }
- else if ((_wordWrap && _leftColumn > 0) || (dSize.size < Viewport.Width + offB.width && tSize.size < Viewport.Width + offB.width))
- {
- if (_leftColumn > 0)
- {
- _leftColumn = 0;
- need = true;
- }
- }
-
- if (CurrentRow < _topRow)
- {
- _topRow = CurrentRow;
- need = true;
- }
- else if (CurrentRow - _topRow >= Viewport.Height + offB.height)
- {
- _topRow = Math.Min (Math.Max (CurrentRow - Viewport.Height + 1, 0), CurrentRow);
- need = true;
- }
- else if (_topRow > 0 && CurrentRow < _topRow)
- {
- _topRow = Math.Max (_topRow - 1, 0);
- need = true;
- }
-
- if (need)
- {
- if (_wrapNeeded)
- {
- WrapTextModel ();
- _wrapNeeded = false;
- }
-
- SetNeedsDraw ();
- }
- else
- {
- if (IsInitialized)
- {
- PositionCursor ();
- }
- }
-
- OnUnwrappedCursorPosition ();
- }
-
- private void AppendClipboard (string text) { Clipboard.Contents += text; }
-
- private PopoverMenu CreateContextMenu ()
- {
- PopoverMenu menu = new (
- new List
- {
- new MenuItem (this, Command.SelectAll, Strings.ctxSelectAll),
- new MenuItem (this, Command.DeleteAll, Strings.ctxDeleteAll),
- new MenuItem (this, Command.Copy, Strings.ctxCopy),
- new MenuItem (this, Command.Cut, Strings.ctxCut),
- new MenuItem (this, Command.Paste, Strings.ctxPaste),
- new MenuItem (this, Command.Undo, Strings.ctxUndo),
- new MenuItem (this, Command.Redo, Strings.ctxRedo)
- });
-
- menu.KeyChanged += ContextMenu_KeyChanged;
-
- return menu;
- }
-
- private void ClearRegion (int left, int top, int right, int bottom)
- {
- for (int row = top; row < bottom; row++)
- {
- Move (left, row);
-
- for (int col = left; col < right; col++)
- {
- AddRune (col, row, (Rune)' ');
- }
- }
- }
-
- //
- // Clears the contents of the selected region
- //
- private void ClearRegion ()
- {
- SetWrapModel ();
-
- long start, end;
- long currentEncoded = ((long)(uint)CurrentRow << 32) | (uint)CurrentColumn;
- GetEncodedRegionBounds (out start, out end);
- var startRow = (int)(start >> 32);
- var maxrow = (int)(end >> 32);
- var startCol = (int)(start & 0xffffffff);
- var endCol = (int)(end & 0xffffffff);
- List line = _model.GetLine (startRow);
-
- _historyText.Add (new () { new (line) }, new (startCol, startRow));
-
- List> removedLines = new ();
-
- if (startRow == maxrow)
- {
- removedLines.Add (new (line));
-
- line.RemoveRange (startCol, endCol - startCol);
- CurrentColumn = startCol;
-
- if (_wordWrap)
- {
- SetNeedsDraw ();
- }
- else
- {
- //QUESTION: Is the below comment still relevant?
- // BUGBUG: customized rect aren't supported now because the Redraw isn't using the Intersect method.
- //SetNeedsDraw (new (0, startRow - topRow, Viewport.Width, startRow - topRow + 1));
- SetNeedsDraw ();
- }
-
- _historyText.Add (
- new (removedLines),
- CursorPosition,
- TextEditingLineStatus.Removed
- );
-
- UpdateWrapModel ();
-
- return;
- }
-
- removedLines.Add (new (line));
-
- line.RemoveRange (startCol, line.Count - startCol);
- List line2 = _model.GetLine (maxrow);
- line.AddRange (line2.Skip (endCol));
-
- for (int row = startRow + 1; row <= maxrow; row++)
- {
- removedLines.Add (new (_model.GetLine (startRow + 1)));
-
- _model.RemoveLine (startRow + 1);
- }
-
- if (currentEncoded == end)
- {
- CurrentRow -= maxrow - startRow;
- }
-
- CurrentColumn = startCol;
-
- _historyText.Add (
- new (removedLines),
- CursorPosition,
- TextEditingLineStatus.Removed
- );
-
- UpdateWrapModel ();
-
- SetNeedsDraw ();
- }
-
- private void ClearSelectedRegion ()
- {
- SetWrapModel ();
-
- if (!_isReadOnly)
- {
- ClearRegion ();
- }
-
- UpdateWrapModel ();
- IsSelecting = false;
- DoNeededAction ();
- }
-
- private void ContextMenu_KeyChanged (object? sender, KeyChangedEventArgs e) { KeyBindings.Replace (e.OldKey, e.NewKey); }
-
- private bool DeleteTextBackwards ()
- {
- SetWrapModel ();
-
- if (CurrentColumn > 0)
- {
- // Delete backwards
- List currentLine = GetCurrentLine ();
-
- _historyText.Add (new () { new (currentLine) }, CursorPosition);
-
- currentLine.RemoveAt (CurrentColumn - 1);
-
- if (_wordWrap)
- {
- _wrapNeeded = true;
- }
-
- CurrentColumn--;
-
- _historyText.Add (
- new () { new (currentLine) },
- CursorPosition,
- TextEditingLineStatus.Replaced
- );
-
- if (CurrentColumn < _leftColumn)
- {
- _leftColumn--;
- SetNeedsDraw ();
- }
- else
- {
- // BUGBUG: customized rect aren't supported now because the Redraw isn't using the Intersect method.
- //SetNeedsDraw (new (0, currentRow - topRow, 1, Viewport.Width));
- SetNeedsDraw ();
- }
- }
- else
- {
- // Merges the current line with the previous one.
- if (CurrentRow == 0)
- {
- return true;
- }
-
- int prowIdx = CurrentRow - 1;
- List prevRow = _model.GetLine (prowIdx);
-
- _historyText.Add (new () { new (prevRow) }, CursorPosition);
-
- List> removedLines = new () { new (prevRow) };
-
- removedLines.Add (new (GetCurrentLine ()));
-
- _historyText.Add (
- removedLines,
- new (CurrentColumn, prowIdx),
- TextEditingLineStatus.Removed
- );
-
- int prevCount = prevRow.Count;
- _model.GetLine (prowIdx).AddRange (GetCurrentLine ());
- _model.RemoveLine (CurrentRow);
-
- if (_wordWrap)
- {
- _wrapNeeded = true;
- }
-
- CurrentRow--;
-
- _historyText.Add (
- new () { GetCurrentLine () },
- new (CurrentColumn, prowIdx),
- TextEditingLineStatus.Replaced
- );
-
- CurrentColumn = prevCount;
- SetNeedsDraw ();
- }
-
- UpdateWrapModel ();
-
- return false;
- }
-
- private bool DeleteTextForwards ()
- {
- SetWrapModel ();
-
- List currentLine = GetCurrentLine ();
-
- if (CurrentColumn == currentLine.Count)
- {
- if (CurrentRow + 1 == _model.Count)
- {
- UpdateWrapModel ();
-
- return true;
- }
-
- _historyText.Add (new () { new (currentLine) }, CursorPosition);
-
- List> removedLines = new () { new (currentLine) };
-
- List nextLine = _model.GetLine (CurrentRow + 1);
-
- removedLines.Add (new (nextLine));
-
- _historyText.Add (removedLines, CursorPosition, TextEditingLineStatus.Removed);
-
- currentLine.AddRange (nextLine);
- _model.RemoveLine (CurrentRow + 1);
-
- _historyText.Add (
- new () { new (currentLine) },
- CursorPosition,
- TextEditingLineStatus.Replaced
- );
-
- if (_wordWrap)
- {
- _wrapNeeded = true;
- }
-
- DoSetNeedsDraw (new (0, CurrentRow - _topRow, Viewport.Width, CurrentRow - _topRow + 1));
- }
- else
- {
- _historyText.Add ([ [.. currentLine]], CursorPosition);
-
- currentLine.RemoveAt (CurrentColumn);
-
- _historyText.Add (
- [ [.. currentLine]],
- CursorPosition,
- TextEditingLineStatus.Replaced
- );
-
- if (_wordWrap)
- {
- _wrapNeeded = true;
- }
-
- DoSetNeedsDraw (
- new (
- CurrentColumn - _leftColumn,
- CurrentRow - _topRow,
- Viewport.Width,
- Math.Max (CurrentRow - _topRow + 1, 0)
- )
- );
- }
-
- UpdateWrapModel ();
-
- return false;
- }
-
- private void DoNeededAction ()
- {
- if (!NeedsDraw && (IsSelecting || _wrapNeeded || !Used))
- {
- SetNeedsDraw ();
- }
-
- if (NeedsDraw)
- {
- Adjust ();
- }
- else
- {
- PositionCursor ();
- OnUnwrappedCursorPosition ();
- }
- }
-
- private void DoSetNeedsDraw (Rectangle rect)
- {
- if (_wrapNeeded)
- {
- SetNeedsDraw ();
- }
- else
- {
- // BUGBUG: customized rect aren't supported now because the Redraw isn't using the Intersect method.
- //SetNeedsDraw (rect);
- SetNeedsDraw ();
- }
- }
-
- private IEnumerable<(int col, int row, Cell rune)> ForwardIterator (int col, int row)
- {
- if (col < 0 || row < 0)
- {
- yield break;
- }
-
- if (row >= _model.Count)
- {
- yield break;
- }
-
- List line = GetCurrentLine ();
-
- if (col >= line.Count)
- {
- yield break;
- }
-
- while (row < _model.Count)
- {
- for (int c = col; c < line.Count; c++)
- {
- yield return (c, row, line [c]);
- }
-
- col = 0;
- row++;
- line = GetCurrentLine ();
- }
- }
-
- private void GenerateSuggestions ()
- {
- List currentLine = GetCurrentLine ();
- int cursorPosition = Math.Min (CurrentColumn, currentLine.Count);
-
- Autocomplete.Context = new (
- currentLine,
- cursorPosition,
- Autocomplete.Context != null
- ? Autocomplete.Context.Canceled
- : false
- );
-
- Autocomplete.GenerateSuggestions (
- Autocomplete.Context
- );
- }
-
- // Returns an encoded region start..end (top 32 bits are the row, low32 the column)
- private void GetEncodedRegionBounds (
- out long start,
- out long end,
- int? startRow = null,
- int? startCol = null,
- int? cRow = null,
- int? cCol = null
- )
- {
- long selection;
- long point;
-
- if (startRow is null || startCol is null || cRow is null || cCol is null)
- {
- selection = ((long)(uint)_selectionStartRow << 32) | (uint)_selectionStartColumn;
- point = ((long)(uint)CurrentRow << 32) | (uint)CurrentColumn;
- }
- else
- {
- selection = ((long)(uint)startRow << 32) | (uint)startCol;
- point = ((long)(uint)cRow << 32) | (uint)cCol;
- }
-
- if (selection > point)
- {
- start = point;
- end = selection;
- }
- else
- {
- start = selection;
- end = point;
- }
- }
-
- //
- // Returns a string with the text in the selected
- // region.
- //
- internal string GetRegion (
- out List> cellsList,
- int? sRow = null,
- int? sCol = null,
- int? cRow = null,
- int? cCol = null,
- TextModel? model = null
- )
- {
- GetEncodedRegionBounds (out long start, out long end, sRow, sCol, cRow, cCol);
-
- cellsList = [];
-
- if (start == end)
- {
- return string.Empty;
- }
-
- var startRow = (int)(start >> 32);
- var maxRow = (int)(end >> 32);
- var startCol = (int)(start & 0xffffffff);
- var endCol = (int)(end & 0xffffffff);
- List line = model is null ? _model.GetLine (startRow) : model.GetLine (startRow);
- List cells;
-
- if (startRow == maxRow)
- {
- cells = line.GetRange (startCol, endCol - startCol);
- cellsList.Add (cells);
-
- return StringFromCells (cells);
- }
-
- cells = line.GetRange (startCol, line.Count - startCol);
- cellsList.Add (cells);
- string res = StringFromCells (cells);
-
- for (int row = startRow + 1; row < maxRow; row++)
- {
- cellsList.AddRange ([]);
- cells = model == null ? _model.GetLine (row) : model.GetLine (row);
- cellsList.Add (cells);
-
- res = res
- + Environment.NewLine
- + StringFromCells (cells);
- }
-
- line = model is null ? _model.GetLine (maxRow) : model.GetLine (maxRow);
- cellsList.AddRange ([]);
- cells = line.GetRange (0, endCol);
- cellsList.Add (cells);
- res = res + Environment.NewLine + StringFromCells (cells);
-
- return res;
- }
-
- private int GetSelectedLength () { return SelectedText.Length; }
-
- private string GetSelectedRegion ()
- {
- int cRow = CurrentRow;
- int cCol = CurrentColumn;
- int startRow = _selectionStartRow;
- int startCol = _selectionStartColumn;
- TextModel model = _model;
-
- if (_wordWrap)
- {
- cRow = _wrapManager!.GetModelLineFromWrappedLines (CurrentRow);
- cCol = _wrapManager.GetModelColFromWrappedLines (CurrentRow, CurrentColumn);
- startRow = _wrapManager.GetModelLineFromWrappedLines (_selectionStartRow);
- startCol = _wrapManager.GetModelColFromWrappedLines (_selectionStartRow, _selectionStartColumn);
- model = _wrapManager.Model;
- }
-
- OnUnwrappedCursorPosition (cRow, cCol);
-
- return GetRegion (out _, startRow, startCol, cRow, cCol, model);
- }
-
- private (int Row, int Col) GetUnwrappedPosition (int line, int col)
- {
- if (WordWrap)
- {
- return new ValueTuple (
- _wrapManager!.GetModelLineFromWrappedLines (line),
- _wrapManager.GetModelColFromWrappedLines (line, col)
- );
- }
-
- return new ValueTuple (line, col);
- }
-
- private void HistoryText_ChangeText (object sender, HistoryTextItemEventArgs obj)
- {
- SetWrapModel ();
-
- if (obj is { })
- {
- int startLine = obj.CursorPosition.Y;
-
- if (obj.RemovedOnAdded is { })
- {
- int offset;
-
- if (obj.IsUndoing)
- {
- offset = Math.Max (obj.RemovedOnAdded.Lines.Count - obj.Lines.Count, 1);
- }
- else
- {
- offset = obj.RemovedOnAdded.Lines.Count - 1;
- }
-
- for (var i = 0; i < offset; i++)
- {
- if (Lines > obj.RemovedOnAdded.CursorPosition.Y)
- {
- _model.RemoveLine (obj.RemovedOnAdded.CursorPosition.Y);
- }
- else
- {
- break;
- }
- }
- }
-
- for (var i = 0; i < obj.Lines.Count; i++)
- {
- if (i == 0 || obj.LineStatus == TextEditingLineStatus.Original || obj.LineStatus == TextEditingLineStatus.Attribute)
- {
- _model.ReplaceLine (startLine, obj.Lines [i]);
- }
- else if (obj is { IsUndoing: true, LineStatus: TextEditingLineStatus.Removed }
- or { IsUndoing: false, LineStatus: TextEditingLineStatus.Added })
- {
- _model.AddLine (startLine, obj.Lines [i]);
- }
- else if (Lines > obj.CursorPosition.Y + 1)
- {
- _model.RemoveLine (obj.CursorPosition.Y + 1);
- }
-
- startLine++;
- }
-
- CursorPosition = obj.FinalCursorPosition;
- }
-
- UpdateWrapModel ();
-
- Adjust ();
- OnContentsChanged ();
- }
-
- private void Insert (Cell cell)
- {
- List line = GetCurrentLine ();
-
- if (Used)
- {
- line.Insert (Math.Min (CurrentColumn, line.Count), cell);
- }
- else
- {
- if (CurrentColumn < line.Count)
- {
- line.RemoveAt (CurrentColumn);
- }
-
- line.Insert (Math.Min (CurrentColumn, line.Count), cell);
- }
-
- int prow = CurrentRow - _topRow;
-
- if (!_wrapNeeded)
- {
- // BUGBUG: customized rect aren't supported now because the Redraw isn't using the Intersect method.
- //SetNeedsDraw (new (0, prow, Math.Max (Viewport.Width, 0), Math.Max (prow + 1, 0)));
- SetNeedsDraw ();
- }
- }
-
- private void InsertAllText (string text, bool fromClipboard = false)
- {
- if (string.IsNullOrEmpty (text))
- {
- return;
- }
-
- List> lines;
-
- if (fromClipboard && text == _copiedText)
- {
- lines = _copiedCellsList;
- }
- else
- {
- // Get selected attribute
- Attribute? attribute = GetSelectedAttribute (CurrentRow, CurrentColumn);
- lines = Cell.StringToLinesOfCells (text, attribute);
- }
-
- if (lines.Count == 0)
- {
- return;
- }
-
- SetWrapModel ();
-
- List line = GetCurrentLine ();
-
- _historyText.Add ([new (line)], CursorPosition);
-
- // Optimize single line
- if (lines.Count == 1)
- {
- line.InsertRange (CurrentColumn, lines [0]);
- CurrentColumn += lines [0].Count;
-
- _historyText.Add (
- [new (line)],
- CursorPosition,
- TextEditingLineStatus.Replaced
- );
-
- if (!_wordWrap && CurrentColumn - _leftColumn > Viewport.Width)
- {
- _leftColumn = Math.Max (CurrentColumn - Viewport.Width + 1, 0);
- }
-
- if (_wordWrap)
- {
- SetNeedsDraw ();
- }
- else
- {
- // BUGBUG: customized rect aren't supported now because the Redraw isn't using the Intersect method.
- //SetNeedsDraw (new (0, currentRow - topRow, Viewport.Width, Math.Max (currentRow - topRow + 1, 0)));
- SetNeedsDraw ();
- }
-
- UpdateWrapModel ();
-
- OnContentsChanged ();
-
- return;
- }
-
- List? rest = null;
- var lastPosition = 0;
-
- if (_model.Count > 0 && line.Count > 0 && !_copyWithoutSelection)
- {
- // Keep a copy of the rest of the line
- int restCount = line.Count - CurrentColumn;
- rest = line.GetRange (CurrentColumn, restCount);
- line.RemoveRange (CurrentColumn, restCount);
- }
-
- // First line is inserted at the current location, the rest is appended
- line.InsertRange (CurrentColumn, lines [0]);
-
- //model.AddLine (currentRow, lines [0]);
-
- List> addedLines = [new (line)];
-
- for (var i = 1; i < lines.Count; i++)
- {
- _model.AddLine (CurrentRow + i, lines [i]);
-
- addedLines.Add ([.. lines [i]]);
- }
-
- if (rest is { })
- {
- List last = _model.GetLine (CurrentRow + lines.Count - 1);
- lastPosition = last.Count;
- last.InsertRange (last.Count, rest);
-
- addedLines.Last ().InsertRange (addedLines.Last ().Count, rest);
- }
-
- _historyText.Add (addedLines, CursorPosition, TextEditingLineStatus.Added);
-
- // Now adjust column and row positions
- CurrentRow += lines.Count - 1;
- CurrentColumn = rest is { } ? lastPosition : lines [^1].Count;
- Adjust ();
-
- _historyText.Add (
- [new (line)],
- CursorPosition,
- TextEditingLineStatus.Replaced
- );
-
- UpdateWrapModel ();
- OnContentsChanged ();
- }
-
- private bool InsertText (Key a, Attribute? attribute = null)
- {
- //So that special keys like tab can be processed
- if (_isReadOnly)
- {
- return true;
- }
-
- SetWrapModel ();
-
- _historyText.Add ([new (GetCurrentLine ())], CursorPosition);
-
- if (IsSelecting)
- {
- ClearSelectedRegion ();
- }
-
- if ((uint)a.KeyCode == '\n')
- {
- _model.AddLine (CurrentRow + 1, []);
- CurrentRow++;
- CurrentColumn = 0;
- }
- else if ((uint)a.KeyCode == '\r')
- {
- CurrentColumn = 0;
- }
- else
- {
- if (Used)
- {
- Insert (new () { Grapheme = a.AsRune.ToString (), Attribute = attribute });
- CurrentColumn++;
-
- if (CurrentColumn >= _leftColumn + Viewport.Width)
- {
- _leftColumn++;
- SetNeedsDraw ();
- }
- }
- else
- {
- Insert (new () { Grapheme = a.AsRune.ToString (), Attribute = attribute });
- CurrentColumn++;
- }
- }
-
- _historyText.Add (
- [new (GetCurrentLine ())],
- CursorPosition,
- TextEditingLineStatus.Replaced
- );
-
- UpdateWrapModel ();
- OnContentsChanged ();
-
- return true;
- }
-
- private void KillToEndOfLine ()
- {
- if (_isReadOnly)
- {
- return;
- }
-
- if (_model.Count == 1 && GetCurrentLine ().Count == 0)
- {
- // Prevents from adding line feeds if there is no more lines.
- return;
- }
-
- SetWrapModel ();
-
- List currentLine = GetCurrentLine ();
- var setLastWasKill = true;
-
- if (currentLine.Count > 0 && CurrentColumn == currentLine.Count)
- {
- UpdateWrapModel ();
-
- DeleteTextForwards ();
-
- return;
- }
-
- _historyText.Add (new () { new (currentLine) }, CursorPosition);
-
- if (currentLine.Count == 0)
- {
- if (CurrentRow < _model.Count - 1)
- {
- List> removedLines = new () { new (currentLine) };
-
- _model.RemoveLine (CurrentRow);
-
- removedLines.Add (new (GetCurrentLine ()));
-
- _historyText.Add (
- new (removedLines),
- CursorPosition,
- TextEditingLineStatus.Removed
- );
- }
-
- if (_model.Count > 0 || _lastWasKill)
- {
- string val = Environment.NewLine;
-
- if (_lastWasKill)
- {
- AppendClipboard (val);
- }
- else
- {
- SetClipboard (val);
- }
- }
-
- if (_model.Count == 0)
- {
- // Prevents from adding line feeds if there is no more lines.
- setLastWasKill = false;
- }
- }
- else
- {
- int restCount = currentLine.Count - CurrentColumn;
- List rest = currentLine.GetRange (CurrentColumn, restCount);
- var val = string.Empty;
- val += StringFromCells (rest);
-
- if (_lastWasKill)
- {
- AppendClipboard (val);
- }
- else
- {
- SetClipboard (val);
- }
-
- currentLine.RemoveRange (CurrentColumn, restCount);
- }
-
- _historyText.Add (
- [ [.. GetCurrentLine ()]],
- CursorPosition,
- TextEditingLineStatus.Replaced
- );
-
- UpdateWrapModel ();
-
- DoSetNeedsDraw (new (0, CurrentRow - _topRow, Viewport.Width, Viewport.Height));
-
- _lastWasKill = setLastWasKill;
- DoNeededAction ();
- }
-
- private void KillToLeftStart ()
- {
- if (_isReadOnly)
- {
- return;
- }
-
- if (_model.Count == 1 && GetCurrentLine ().Count == 0)
- {
- // Prevents from adding line feeds if there is no more lines.
- return;
- }
-
- SetWrapModel ();
-
- List currentLine = GetCurrentLine ();
- var setLastWasKill = true;
-
- if (currentLine.Count > 0 && CurrentColumn == 0)
- {
- UpdateWrapModel ();
-
- DeleteTextBackwards ();
-
- return;
- }
-
- _historyText.Add ([ [.. currentLine]], CursorPosition);
-
- if (currentLine.Count == 0)
- {
- if (CurrentRow > 0)
- {
- _model.RemoveLine (CurrentRow);
-
- if (_model.Count > 0 || _lastWasKill)
- {
- string val = Environment.NewLine;
-
- if (_lastWasKill)
- {
- AppendClipboard (val);
- }
- else
- {
- SetClipboard (val);
- }
- }
-
- if (_model.Count == 0)
- {
- // Prevents from adding line feeds if there is no more lines.
- setLastWasKill = false;
- }
-
- CurrentRow--;
- currentLine = _model.GetLine (CurrentRow);
-
- List> removedLine =
- [
- [..currentLine],
- []
- ];
-
- _historyText.Add (
- [.. removedLine],
- CursorPosition,
- TextEditingLineStatus.Removed
- );
-
- CurrentColumn = currentLine.Count;
- }
- }
- else
- {
- int restCount = CurrentColumn;
- List rest = currentLine.GetRange (0, restCount);
- var val = string.Empty;
- val += StringFromCells (rest);
-
- if (_lastWasKill)
- {
- AppendClipboard (val);
- }
- else
- {
- SetClipboard (val);
- }
-
- currentLine.RemoveRange (0, restCount);
- CurrentColumn = 0;
- }
-
- _historyText.Add (
- [ [.. GetCurrentLine ()]],
- CursorPosition,
- TextEditingLineStatus.Replaced
- );
-
- UpdateWrapModel ();
-
- DoSetNeedsDraw (new (0, CurrentRow - _topRow, Viewport.Width, Viewport.Height));
-
- _lastWasKill = setLastWasKill;
- DoNeededAction ();
- }
-
- private void KillWordBackward ()
- {
- if (_isReadOnly)
- {
- return;
- }
-
- SetWrapModel ();
-
- List currentLine = GetCurrentLine ();
-
- _historyText.Add ([ [.. GetCurrentLine ()]], CursorPosition);
-
- if (CurrentColumn == 0)
- {
- DeleteTextBackwards ();
-
- _historyText.ReplaceLast (
- [ [.. GetCurrentLine ()]],
- CursorPosition,
- TextEditingLineStatus.Replaced
- );
-
- UpdateWrapModel ();
-
- return;
- }
-
- (int col, int row)? newPos = _model.WordBackward (CurrentColumn, CurrentRow, UseSameRuneTypeForWords);
-
- if (newPos.HasValue && CurrentRow == newPos.Value.row)
- {
- int restCount = CurrentColumn - newPos.Value.col;
- currentLine.RemoveRange (newPos.Value.col, restCount);
-
- if (_wordWrap)
- {
- _wrapNeeded = true;
- }
-
- CurrentColumn = newPos.Value.col;
- }
- else if (newPos.HasValue)
- {
- int restCount;
-
- if (newPos.Value.row == CurrentRow)
- {
- restCount = currentLine.Count - CurrentColumn;
- currentLine.RemoveRange (CurrentColumn, restCount);
- }
- else
- {
- while (CurrentRow != newPos.Value.row)
- {
- restCount = currentLine.Count;
- currentLine.RemoveRange (0, restCount);
-
- CurrentRow--;
- currentLine = GetCurrentLine ();
- }
- }
-
- if (_wordWrap)
- {
- _wrapNeeded = true;
- }
-
- CurrentColumn = newPos.Value.col;
- CurrentRow = newPos.Value.row;
- }
-
- _historyText.Add (
- [ [.. GetCurrentLine ()]],
- CursorPosition,
- TextEditingLineStatus.Replaced
- );
-
- UpdateWrapModel ();
-
- DoSetNeedsDraw (new (0, CurrentRow - _topRow, Viewport.Width, Viewport.Height));
- DoNeededAction ();
- }
-
- private void KillWordForward ()
- {
- if (_isReadOnly)
- {
- return;
- }
-
- SetWrapModel ();
-
- List currentLine = GetCurrentLine ();
-
- _historyText.Add ([ [.. GetCurrentLine ()]], CursorPosition);
-
- if (currentLine.Count == 0 || CurrentColumn == currentLine.Count)
- {
- DeleteTextForwards ();
-
- _historyText.ReplaceLast (
- [ [.. GetCurrentLine ()]],
- CursorPosition,
- TextEditingLineStatus.Replaced
- );
-
- UpdateWrapModel ();
-
- return;
- }
-
- (int col, int row)? newPos = _model.WordForward (CurrentColumn, CurrentRow, UseSameRuneTypeForWords);
- var restCount = 0;
-
- if (newPos.HasValue && CurrentRow == newPos.Value.row)
- {
- restCount = newPos.Value.col - CurrentColumn;
- currentLine.RemoveRange (CurrentColumn, restCount);
- }
- else if (newPos.HasValue)
- {
- restCount = currentLine.Count - CurrentColumn;
- currentLine.RemoveRange (CurrentColumn, restCount);
- }
-
- if (_wordWrap)
- {
- _wrapNeeded = true;
- }
-
- _historyText.Add (
- [ [.. GetCurrentLine ()]],
- CursorPosition,
- TextEditingLineStatus.Replaced
- );
-
- UpdateWrapModel ();
-
- DoSetNeedsDraw (new (0, CurrentRow - _topRow, Viewport.Width, Viewport.Height));
- DoNeededAction ();
- }
-
- private void Model_LinesLoaded (object sender, EventArgs e)
- {
- // This call is not needed. Model_LinesLoaded gets invoked when
- // model.LoadString (value) is called. LoadString is called from one place
- // (Text.set) and historyText.Clear() is called immediately after.
- // If this call happens, HistoryText_ChangeText will get called multiple times
- // when Text is set, which is wrong.
- //historyText.Clear (Text);
-
- if (!_multiline && !IsInitialized)
- {
- CurrentColumn = Text.GetRuneCount ();
- _leftColumn = CurrentColumn > Viewport.Width + 1 ? CurrentColumn - Viewport.Width + 1 : 0;
- }
- }
-
- private void MoveBottomEnd ()
- {
- ResetAllTrack ();
-
- if (_shiftSelecting && IsSelecting)
- {
- StopSelecting ();
- }
-
- MoveEnd ();
- }
-
- private void MoveBottomEndExtend ()
- {
- ResetAllTrack ();
- StartSelecting ();
- MoveEnd ();
- }
-
- private bool MoveDown ()
- {
- if (CurrentRow + 1 < _model.Count)
- {
- if (_columnTrack == -1)
- {
- _columnTrack = CurrentColumn;
- }
-
- CurrentRow++;
-
- if (CurrentRow >= _topRow + Viewport.Height)
- {
- _topRow++;
- SetNeedsDraw ();
- }
-
- TrackColumn ();
- PositionCursor ();
- }
- else if (CurrentRow > Viewport.Height)
- {
- Adjust ();
- }
- else
- {
- return false;
- }
-
- DoNeededAction ();
-
- return true;
- }
-
- private void MoveEndOfLine ()
- {
- List currentLine = GetCurrentLine ();
- CurrentColumn = currentLine.Count;
- DoNeededAction ();
- }
-
- private bool MoveLeft ()
- {
- if (CurrentColumn > 0)
- {
- CurrentColumn--;
- }
- else
- {
- if (CurrentRow > 0)
- {
- CurrentRow--;
-
- if (CurrentRow < _topRow)
- {
- _topRow--;
- SetNeedsDraw ();
- }
-
- List currentLine = GetCurrentLine ();
- CurrentColumn = Math.Max (currentLine.Count - (ReadOnly ? 1 : 0), 0);
- }
- else
- {
- return false;
- }
- }
-
- DoNeededAction ();
-
- return true;
- }
-
- private void MovePageDown ()
- {
- int nPageDnShift = Viewport.Height - 1;
-
- if (CurrentRow >= 0 && CurrentRow < _model.Count)
- {
- if (_columnTrack == -1)
- {
- _columnTrack = CurrentColumn;
- }
-
- CurrentRow = CurrentRow + nPageDnShift > _model.Count
- ? _model.Count > 0 ? _model.Count - 1 : 0
- : CurrentRow + nPageDnShift;
-
- if (_topRow < CurrentRow - nPageDnShift)
- {
- _topRow = CurrentRow >= _model.Count
- ? CurrentRow - nPageDnShift
- : _topRow + nPageDnShift;
- SetNeedsDraw ();
- }
-
- TrackColumn ();
- PositionCursor ();
- }
-
- DoNeededAction ();
- }
-
- private void MovePageUp ()
- {
- int nPageUpShift = Viewport.Height - 1;
-
- if (CurrentRow > 0)
- {
- if (_columnTrack == -1)
- {
- _columnTrack = CurrentColumn;
- }
-
- CurrentRow = CurrentRow - nPageUpShift < 0 ? 0 : CurrentRow - nPageUpShift;
-
- if (CurrentRow < _topRow)
- {
- _topRow = _topRow - nPageUpShift < 0 ? 0 : _topRow - nPageUpShift;
- SetNeedsDraw ();
- }
-
- TrackColumn ();
- PositionCursor ();
- }
-
- DoNeededAction ();
- }
-
- private bool MoveRight ()
- {
- List currentLine = GetCurrentLine ();
-
- if ((ReadOnly ? CurrentColumn + 1 : CurrentColumn) < currentLine.Count)
- {
- CurrentColumn++;
- }
- else
- {
- if (CurrentRow + 1 < _model.Count)
- {
- CurrentRow++;
- CurrentColumn = 0;
-
- if (CurrentRow >= _topRow + Viewport.Height)
- {
- _topRow++;
- SetNeedsDraw ();
- }
- }
- else
- {
- return false;
- }
- }
-
- DoNeededAction ();
-
- return true;
- }
-
- private void MoveLeftStart ()
- {
- if (_leftColumn > 0)
- {
- SetNeedsDraw ();
- }
-
- CurrentColumn = 0;
- _leftColumn = 0;
- DoNeededAction ();
- }
-
- private void MoveTopHome ()
- {
- ResetAllTrack ();
-
- if (_shiftSelecting && IsSelecting)
- {
- StopSelecting ();
- }
-
- MoveHome ();
- }
-
- private void MoveTopHomeExtend ()
- {
- ResetColumnTrack ();
- StartSelecting ();
- MoveHome ();
- }
-
- private bool MoveUp ()
- {
- if (CurrentRow > 0)
- {
- if (_columnTrack == -1)
- {
- _columnTrack = CurrentColumn;
- }
-
- CurrentRow--;
-
- if (CurrentRow < _topRow)
- {
- _topRow--;
- SetNeedsDraw ();
- }
-
- TrackColumn ();
- PositionCursor ();
- }
- else
- {
- return false;
- }
-
- DoNeededAction ();
-
- return true;
- }
-
- private void MoveWordBackward ()
- {
- (int col, int row)? newPos = _model.WordBackward (CurrentColumn, CurrentRow, UseSameRuneTypeForWords);
-
- if (newPos.HasValue)
- {
- CurrentColumn = newPos.Value.col;
- CurrentRow = newPos.Value.row;
- }
-
- DoNeededAction ();
- }
-
- private void MoveWordForward ()
- {
- (int col, int row)? newPos = _model.WordForward (CurrentColumn, CurrentRow, UseSameRuneTypeForWords);
-
- if (newPos.HasValue)
- {
- CurrentColumn = newPos.Value.col;
- CurrentRow = newPos.Value.row;
- }
-
- DoNeededAction ();
- }
-
- private (int width, int height) OffSetBackground ()
- {
- var w = 0;
- var h = 0;
-
- if (SuperView?.Viewport.Right - Viewport.Right < 0)
- {
- w = SuperView!.Viewport.Right - Viewport.Right - 1;
- }
-
- if (SuperView?.Viewport.Bottom - Viewport.Bottom < 0)
- {
- h = SuperView!.Viewport.Bottom - Viewport.Bottom - 1;
- }
-
- return (w, h);
- }
-
- private bool PointInSelection (int col, int row)
- {
- long start, end;
- GetEncodedRegionBounds (out start, out end);
- long q = ((long)(uint)row << 32) | (uint)col;
-
- return q >= start && q <= end - 1;
- }
-
- private void ProcessAutocomplete ()
- {
- if (_isDrawing)
- {
- return;
- }
-
- if (_clickWithSelecting)
- {
- _clickWithSelecting = false;
-
- return;
- }
-
- if (SelectedLength > 0)
- {
- return;
- }
-
- // draw autocomplete
- GenerateSuggestions ();
-
- var renderAt = new Point (
- Autocomplete.Context.CursorPosition,
- Autocomplete.PopupInsideContainer
- ? CursorPosition.Y + 1 - TopRow
- : 0
- );
-
- Autocomplete.RenderOverlay (renderAt);
- }
-
- private bool ProcessBackTab ()
- {
- ResetColumnTrack ();
-
- if (!AllowsTab || _isReadOnly)
- {
- return false;
- }
-
- if (CurrentColumn > 0)
- {
- SetWrapModel ();
-
- List currentLine = GetCurrentLine ();
-
- if (currentLine.Count > 0 && currentLine[CurrentColumn - 1].Grapheme == "\t")
- {
- _historyText.Add (new () { new (currentLine) }, CursorPosition);
-
- currentLine.RemoveAt (CurrentColumn - 1);
- CurrentColumn--;
-
- _historyText.Add (
- new () { new (GetCurrentLine ()) },
- CursorPosition,
- TextEditingLineStatus.Replaced
- );
- }
-
- SetNeedsDraw ();
-
- UpdateWrapModel ();
- }
-
- DoNeededAction ();
-
- return true;
- }
-
- private void ProcessCopy ()
- {
- ResetColumnTrack ();
- Copy ();
- }
-
- private void ProcessCut ()
- {
- ResetColumnTrack ();
- Cut ();
- }
-
- private void ProcessDeleteCharLeft ()
- {
- ResetColumnTrack ();
- DeleteCharLeft ();
- }
-
- private void ProcessDeleteCharRight ()
- {
- ResetColumnTrack ();
- DeleteCharRight ();
- }
-
- private Attribute? GetSelectedAttribute (int row, int col)
- {
- if (!InheritsPreviousAttribute || (Lines == 1 && GetLine (Lines).Count == 0))
- {
- return null;
- }
-
- List line = GetLine (row);
- int foundRow = row;
-
- while (line.Count == 0)
- {
- if (foundRow == 0 && line.Count == 0)
- {
- return null;
- }
-
- foundRow--;
- line = GetLine (foundRow);
- }
-
- int foundCol = foundRow < row ? line.Count - 1 : Math.Min (col, line.Count - 1);
-
- Cell cell = line [foundCol];
-
- return cell.Attribute;
- }
-
- // If InheritsPreviousScheme is enabled this method will check if the rune cell on
- // the row and col location and around has a not null scheme. If it's null will set it with
- // the very most previous valid scheme.
- private void ProcessInheritsPreviousScheme (int row, int col)
- {
- if (!InheritsPreviousAttribute || (Lines == 1 && GetLine (Lines).Count == 0))
- {
- return;
- }
-
- List line = GetLine (row);
- List lineToSet = line;
-
- while (line.Count == 0)
- {
- if (row == 0 && line.Count == 0)
- {
- return;
- }
-
- row--;
- line = GetLine (row);
- lineToSet = line;
- }
-
- int colWithColor = Math.Max (Math.Min (col - 2, line.Count - 1), 0);
- Cell cell = line [colWithColor];
- int colWithoutColor = Math.Max (col - 1, 0);
-
- Cell lineTo = lineToSet [colWithoutColor];
-
- if (cell.Attribute is { } && colWithColor == 0 && lineTo.Attribute is { })
- {
- for (int r = row - 1; r > -1; r--)
- {
- List l = GetLine (r);
-
- for (int c = l.Count - 1; c > -1; c--)
- {
- Cell cell1 = l [c];
-
- if (cell1.Attribute is null)
- {
- cell1.Attribute = cell.Attribute;
- l [c] = cell1;
- }
- else
- {
- return;
- }
- }
- }
-
- return;
- }
-
- if (cell.Attribute is null)
- {
- for (int r = row; r > -1; r--)
- {
- List l = GetLine (r);
-
- colWithColor = l.FindLastIndex (
- colWithColor > -1 ? colWithColor : l.Count - 1,
- c => c.Attribute != null
- );
-
- if (colWithColor > -1 && l [colWithColor].Attribute is { })
- {
- cell = l [colWithColor];
-
- break;
- }
- }
- }
- else
- {
- int cRow = row;
-
- while (cell.Attribute is null)
- {
- if ((colWithColor == 0 || cell.Attribute is null) && cRow > 0)
- {
- line = GetLine (--cRow);
- colWithColor = line.Count - 1;
- cell = line [colWithColor];
- }
- else if (cRow == 0 && colWithColor < line.Count)
- {
- cell = line [colWithColor + 1];
- }
- }
- }
-
- if (cell.Attribute is { } && colWithColor > -1 && colWithoutColor < lineToSet.Count && lineTo.Attribute is null)
- {
- while (lineTo.Attribute is null)
- {
- lineTo.Attribute = cell.Attribute;
- lineToSet [colWithoutColor] = lineTo;
- colWithoutColor--;
-
- if (colWithoutColor == -1 && row > 0)
- {
- lineToSet = GetLine (--row);
- colWithoutColor = lineToSet.Count - 1;
- }
- }
- }
- }
-
- private void ProcessKillWordBackward ()
- {
- ResetColumnTrack ();
- KillWordBackward ();
- }
-
- private void ProcessKillWordForward ()
- {
- ResetColumnTrack ();
- StopSelecting ();
- KillWordForward ();
- }
-
- private void ProcessMouseClick (MouseEventArgs ev, out List line)
- {
- List? r = null;
-
- if (_model.Count > 0)
- {
- int maxCursorPositionableLine = Math.Max (_model.Count - 1 - _topRow, 0);
-
- if (Math.Max (ev.Position.Y, 0) > maxCursorPositionableLine)
- {
- CurrentRow = maxCursorPositionableLine + _topRow;
- }
- else
- {
- CurrentRow = Math.Max (ev.Position.Y + _topRow, 0);
- }
-
- r = GetCurrentLine ();
- int idx = TextModel.GetColFromX (r, _leftColumn, Math.Max (ev.Position.X, 0), TabWidth);
-
- if (idx - _leftColumn >= r.Count)
- {
- CurrentColumn = Math.Max (r.Count - _leftColumn - (ReadOnly ? 1 : 0), 0);
- }
- else
- {
- CurrentColumn = idx + _leftColumn;
- }
- }
-
- line = r!;
- }
-
- private bool ProcessMoveDown ()
- {
- ResetContinuousFindTrack ();
-
- if (_shiftSelecting && IsSelecting)
- {
- StopSelecting ();
- }
-
- return MoveDown ();
- }
-
- private void ProcessMoveDownExtend ()
- {
- ResetColumnTrack ();
- StartSelecting ();
- MoveDown ();
- }
-
- private void ProcessMoveEndOfLine ()
- {
- ResetAllTrack ();
-
- if (_shiftSelecting && IsSelecting)
- {
- StopSelecting ();
- }
-
- MoveEndOfLine ();
- }
-
- private void ProcessMoveRightEndExtend ()
- {
- ResetAllTrack ();
- StartSelecting ();
- MoveEndOfLine ();
- }
-
- private bool ProcessMoveLeft ()
- {
- // if the user presses Left (without any control keys) and they are at the start of the text
- if (CurrentColumn == 0 && CurrentRow == 0)
- {
- if (IsSelecting)
- {
- StopSelecting ();
-
- return true;
- }
-
- // do not respond (this lets the key press fall through to navigation system - which usually changes focus backward)
- return false;
- }
-
- ResetAllTrack ();
-
- if (_shiftSelecting && IsSelecting)
- {
- StopSelecting ();
- }
-
- MoveLeft ();
-
- return true;
- }
-
- private void ProcessMoveLeftExtend ()
- {
- ResetAllTrack ();
- StartSelecting ();
- MoveLeft ();
- }
-
- private bool ProcessMoveRight ()
- {
- // if the user presses Right (without any control keys)
- // determine where the last cursor position in the text is
- int lastRow = _model.Count - 1;
- int lastCol = _model.GetLine (lastRow).Count;
-
- // if they are at the very end of all the text do not respond (this lets the key press fall through to navigation system - which usually changes focus forward)
- if (CurrentColumn == lastCol && CurrentRow == lastRow)
- {
- // Unless they have text selected
- if (IsSelecting)
- {
- // In which case clear
- StopSelecting ();
-
- return true;
- }
-
- return false;
- }
-
- ResetAllTrack ();
-
- if (_shiftSelecting && IsSelecting)
- {
- StopSelecting ();
- }
-
- MoveRight ();
-
- return true;
- }
-
- private void ProcessMoveRightExtend ()
- {
- ResetAllTrack ();
- StartSelecting ();
- MoveRight ();
- }
-
- private void ProcessMoveLeftStart ()
- {
- ResetAllTrack ();
-
- if (_shiftSelecting && IsSelecting)
- {
- StopSelecting ();
- }
-
- MoveLeftStart ();
- }
-
- private void ProcessMoveLeftStartExtend ()
- {
- ResetAllTrack ();
- StartSelecting ();
- MoveLeftStart ();
- }
-
- private bool ProcessMoveUp ()
- {
- ResetContinuousFindTrack ();
-
- if (_shiftSelecting && IsSelecting)
- {
- StopSelecting ();
- }
-
- return MoveUp ();
- }
-
- private void ProcessMoveUpExtend ()
- {
- ResetColumnTrack ();
- StartSelecting ();
- MoveUp ();
- }
-
- private void ProcessMoveWordBackward ()
- {
- ResetAllTrack ();
-
- if (_shiftSelecting && IsSelecting)
- {
- StopSelecting ();
- }
-
- MoveWordBackward ();
- }
-
- private void ProcessMoveWordBackwardExtend ()
- {
- ResetAllTrack ();
- StartSelecting ();
- MoveWordBackward ();
- }
-
- private void ProcessMoveWordForward ()
- {
- ResetAllTrack ();
-
- if (_shiftSelecting && IsSelecting)
- {
- StopSelecting ();
- }
-
- MoveWordForward ();
- }
-
- private void ProcessMoveWordForwardExtend ()
- {
- ResetAllTrack ();
- StartSelecting ();
- MoveWordForward ();
- }
-
- private void ProcessPageDown ()
- {
- ResetColumnTrack ();
-
- if (_shiftSelecting && IsSelecting)
- {
- StopSelecting ();
- }
-
- MovePageDown ();
- }
-
- private void ProcessPageDownExtend ()
- {
- ResetColumnTrack ();
- StartSelecting ();
- MovePageDown ();
- }
-
- private void ProcessPageUp ()
- {
- ResetColumnTrack ();
-
- if (_shiftSelecting && IsSelecting)
- {
- StopSelecting ();
- }
-
- MovePageUp ();
- }
-
- private void ProcessPageUpExtend ()
- {
- ResetColumnTrack ();
- StartSelecting ();
- MovePageUp ();
- }
-
- private void ProcessPaste ()
- {
- ResetColumnTrack ();
-
- if (_isReadOnly)
- {
- return;
- }
-
- Paste ();
- }
-
- private bool ProcessEnterKey (ICommandContext? commandContext)
- {
- ResetColumnTrack ();
-
- if (_isReadOnly)
- {
- return false;
- }
-
- if (!AllowsReturn)
- {
- // By Default pressing ENTER should be ignored (OnAccept will return false or null). Only cancel if the
- // event was fired and set Cancel = true.
- return RaiseAccepting (commandContext) is null or false;
- }
-
- SetWrapModel ();
-
- List currentLine = GetCurrentLine ();
-
- _historyText.Add (new () { new (currentLine) }, CursorPosition);
-
- if (IsSelecting)
- {
- ClearSelectedRegion ();
- currentLine = GetCurrentLine ();
- }
-
- int restCount = currentLine.Count - CurrentColumn;
- List rest = currentLine.GetRange (CurrentColumn, restCount);
- currentLine.RemoveRange (CurrentColumn, restCount);
-
- List> addedLines = new () { new (currentLine) };
-
- _model.AddLine (CurrentRow + 1, rest);
-
- addedLines.Add (new (_model.GetLine (CurrentRow + 1)));
-
- _historyText.Add (addedLines, CursorPosition, TextEditingLineStatus.Added);
-
- CurrentRow++;
-
- var fullNeedsDraw = false;
-
- if (CurrentRow >= _topRow + Viewport.Height)
- {
- _topRow++;
- fullNeedsDraw = true;
- }
-
- CurrentColumn = 0;
-
- _historyText.Add (
- new () { new (GetCurrentLine ()) },
- CursorPosition,
- TextEditingLineStatus.Replaced
- );
-
- if (!_wordWrap && CurrentColumn < _leftColumn)
- {
- fullNeedsDraw = true;
- _leftColumn = 0;
- }
-
- if (fullNeedsDraw)
- {
- SetNeedsDraw ();
- }
- else
- {
- // BUGBUG: customized rect aren't supported now because the Redraw isn't using the Intersect method.
- //SetNeedsDraw (new (0, currentRow - topRow, 2, Viewport.Height));
- SetNeedsDraw ();
- }
-
- UpdateWrapModel ();
-
- DoNeededAction ();
- OnContentsChanged ();
-
- return true;
- }
-
- private void ProcessSelectAll ()
- {
- ResetColumnTrack ();
- SelectAll ();
- }
-
- private void ProcessSetOverwrite ()
- {
- ResetColumnTrack ();
- SetOverwrite (!Used);
- }
-
- private bool ProcessTab ()
- {
- ResetColumnTrack ();
-
- if (!AllowsTab || _isReadOnly)
- {
- return false;
- }
-
- InsertText (new Key ((KeyCode)'\t'));
- DoNeededAction ();
-
- return true;
- }
-
- private void ResetAllTrack ()
- {
- // Handle some state here - whether the last command was a kill
- // operation and the column tracking (up/down)
- _lastWasKill = false;
- _columnTrack = -1;
- _continuousFind = false;
- }
-
- private void ResetColumnTrack ()
- {
- // Handle some state here - whether the last command was a kill
- // operation and the column tracking (up/down)
- _lastWasKill = false;
- _columnTrack = -1;
- }
-
- private void ResetContinuousFind ()
- {
- if (!_continuousFind)
- {
- int col = IsSelecting ? _selectionStartColumn : CurrentColumn;
- int row = IsSelecting ? _selectionStartRow : CurrentRow;
- _model.ResetContinuousFind (new (col, row));
- }
- }
-
- private void ResetContinuousFindTrack ()
- {
- // Handle some state here - whether the last command was a kill
- // operation and the column tracking (up/down)
- _lastWasKill = false;
- _continuousFind = false;
- }
-
- private void ResetPosition ()
- {
- _topRow = _leftColumn = CurrentRow = CurrentColumn = 0;
- StopSelecting ();
- }
-
- private void SetClipboard (string text)
- {
- if (text is { })
- {
- Clipboard.Contents = text;
- }
- }
-
- private bool SetFoundText (
- string text,
- (Point current, bool found) foundPos,
- string? textToReplace = null,
- bool replace = false,
- bool replaceAll = false
- )
- {
- if (foundPos.found)
- {
- StartSelecting ();
- _selectionStartColumn = foundPos.current.X;
- _selectionStartRow = foundPos.current.Y;
-
- if (!replaceAll)
- {
- CurrentColumn = _selectionStartColumn + text.GetRuneCount ();
- }
- else
- {
- CurrentColumn = _selectionStartColumn + textToReplace!.GetRuneCount ();
- }
-
- CurrentRow = foundPos.current.Y;
-
- if (!_isReadOnly && replace)
- {
- Adjust ();
- ClearSelectedRegion ();
- InsertAllText (textToReplace!);
- StartSelecting ();
- _selectionStartColumn = CurrentColumn - textToReplace!.GetRuneCount ();
- }
- else
- {
- UpdateWrapModel ();
- SetNeedsDraw ();
- Adjust ();
- }
-
- _continuousFind = true;
-
- return foundPos.found;
- }
-
- UpdateWrapModel ();
- _continuousFind = false;
-
- return foundPos.found;
- }
-
- private void SetOverwrite (bool overwrite)
- {
- Used = overwrite;
- SetNeedsDraw ();
- DoNeededAction ();
- }
-
- private void SetValidUsedColor (Attribute? attribute)
- {
- // BUGBUG: (v2 truecolor) This code depends on 8-bit color names; disabling for now
- //if ((scheme!.HotNormal.Foreground & scheme.Focus.Background) == scheme.Focus.Foreground) {
- SetAttribute (new (attribute!.Value.Background, attribute!.Value.Foreground, attribute!.Value.Style));
- }
-
- /// Restore from original model.
- private void SetWrapModel ([CallerMemberName] string? caller = null)
- {
- if (_currentCaller is { })
- {
- return;
- }
-
- if (_wordWrap)
- {
- _currentCaller = caller;
-
- CurrentColumn = _wrapManager!.GetModelColFromWrappedLines (CurrentRow, CurrentColumn);
- CurrentRow = _wrapManager.GetModelLineFromWrappedLines (CurrentRow);
-
- _selectionStartColumn =
- _wrapManager.GetModelColFromWrappedLines (_selectionStartRow, _selectionStartColumn);
- _selectionStartRow = _wrapManager.GetModelLineFromWrappedLines (_selectionStartRow);
- _model = _wrapManager.Model;
- }
- }
-
- private void ShowContextMenu (Point? mousePosition)
- {
- if (!Equals (_currentCulture, Thread.CurrentThread.CurrentUICulture))
- {
- _currentCulture = Thread.CurrentThread.CurrentUICulture;
- }
-
- if (mousePosition is null)
- {
- mousePosition = ViewportToScreen (new Point (CursorPosition.X, CursorPosition.Y));
- }
-
- ContextMenu?.MakeVisible (mousePosition);
- }
-
- private void StartSelecting ()
- {
- if (_shiftSelecting && IsSelecting)
- {
- return;
- }
-
- _shiftSelecting = true;
- IsSelecting = true;
- _selectionStartColumn = CurrentColumn;
- _selectionStartRow = CurrentRow;
- }
-
- private void StopSelecting ()
- {
- if (IsSelecting)
- {
- SetNeedsDraw ();
- }
-
- _shiftSelecting = false;
- IsSelecting = false;
- _isButtonShift = false;
- }
-
- private string StringFromCells (List cells)
- {
- ArgumentNullException.ThrowIfNull (cells);
-
- var size = 0;
- foreach (Cell cell in cells)
- {
- string t = cell.Grapheme;
- size += Encoding.Unicode.GetByteCount (t);
- }
-
- byte [] encoded = new byte [size];
- var offset = 0;
- foreach (Cell cell in cells)
- {
- string t = cell.Grapheme;
- int bytesWritten = Encoding.Unicode.GetBytes (t, 0, t.Length, encoded, offset);
- offset += bytesWritten;
- }
-
- // decode using the same encoding and the bytes actually written
- return Encoding.Unicode.GetString (encoded, 0, offset);
- }
-
- private void TextView_SuperViewChanged (object sender, SuperViewChangedEventArgs e)
- {
- if (e.SuperView is { })
- {
- if (Autocomplete.HostControl is null)
- {
- Autocomplete.HostControl = this;
- }
- }
- else
- {
- Autocomplete.HostControl = null;
- }
- }
-
- private void TextView_Initialized (object sender, EventArgs e)
- {
- if (Autocomplete.HostControl is null)
- {
- Autocomplete.HostControl = this;
- }
-
-
- ContextMenu = CreateContextMenu ();
- App?.Popover?.Register (ContextMenu);
- KeyBindings.Add (ContextMenu.Key, Command.Context);
-
- OnContentsChanged ();
- }
-
- private void TextView_LayoutComplete (object? sender, LayoutEventArgs e)
- {
- WrapTextModel ();
- Adjust ();
- }
-
- private void ToggleSelecting ()
- {
- ResetColumnTrack ();
- IsSelecting = !IsSelecting;
- _selectionStartColumn = CurrentColumn;
- _selectionStartRow = CurrentRow;
- }
-
- // Tries to snap the cursor to the tracking column
- private void TrackColumn ()
- {
- // Now track the column
- List line = GetCurrentLine ();
-
- if (line.Count < _columnTrack)
- {
- CurrentColumn = line.Count;
- }
- else if (_columnTrack != -1)
- {
- CurrentColumn = _columnTrack;
- }
- else if (CurrentColumn > line.Count)
- {
- CurrentColumn = line.Count;
- }
-
- Adjust ();
- }
-
- /// Update the original model.
- private void UpdateWrapModel ([CallerMemberName] string? caller = null)
- {
- if (_currentCaller is { } && _currentCaller != caller)
- {
- return;
- }
-
- if (_wordWrap)
- {
- _currentCaller = null;
-
- _wrapManager!.UpdateModel (
- _model,
- out int nRow,
- out int nCol,
- out int nStartRow,
- out int nStartCol,
- CurrentRow,
- CurrentColumn,
- _selectionStartRow,
- _selectionStartColumn,
- true
- );
- CurrentRow = nRow;
- CurrentColumn = nCol;
- _selectionStartRow = nStartRow;
- _selectionStartColumn = nStartCol;
- _wrapNeeded = true;
-
- SetNeedsDraw ();
- }
-
- if (_currentCaller is { })
- {
- throw new InvalidOperationException (
- $"WordWrap settings was changed after the {_currentCaller} call."
- );
- }
- }
-
- private void WrapTextModel ()
- {
- if (_wordWrap && _wrapManager is { })
- {
- _model = _wrapManager.WrapModel (
- Math.Max (Viewport.Width - (ReadOnly ? 0 : 1), 0), // For the cursor on the last column of a line
- out int nRow,
- out int nCol,
- out int nStartRow,
- out int nStartCol,
- CurrentRow,
- CurrentColumn,
- _selectionStartRow,
- _selectionStartColumn,
- _tabWidth
- );
- CurrentRow = nRow;
- CurrentColumn = nCol;
- _selectionStartRow = nStartRow;
- _selectionStartColumn = nStartCol;
- SetNeedsDraw ();
- }
- }
-
- ///
- public bool EnableForDesign ()
- {
- Text = """
- TextView provides a fully featured multi-line text editor.
- It supports word wrap and history for undo.
- """;
-
- return true;
- }
-
-
- ///
- protected override void Dispose (bool disposing)
- {
- if (disposing && ContextMenu is { })
- {
- ContextMenu.Visible = false;
- ContextMenu.Dispose ();
- ContextMenu = null;
- }
-
- base.Dispose (disposing);
- }
-}
-
-///
-/// Renders an overlay on another view at a given point that allows selecting from a range of 'autocomplete'
-/// options. An implementation on a TextView.
-///
-public class TextViewAutocomplete : PopupAutocomplete
-{
- ///
- protected override void DeleteTextBackwards () { ((TextView)HostControl!).DeleteCharLeft (); }
-
- ///
- protected override void InsertText (string accepted) { ((TextView)HostControl!).InsertText (accepted); }
-
- ///
- protected override void SetCursorPosition (int column)
- {
- ((TextView)HostControl!).CursorPosition =
- new (column, ((TextView)HostControl).CurrentRow);
- }
-}
diff --git a/Terminal.Gui/Views/TextInput/ContentsChangedEventArgs.cs b/Terminal.Gui/Views/TextInput/TextView/ContentsChangedEventArgs.cs
similarity index 100%
rename from Terminal.Gui/Views/TextInput/ContentsChangedEventArgs.cs
rename to Terminal.Gui/Views/TextInput/TextView/ContentsChangedEventArgs.cs
diff --git a/Terminal.Gui/Views/TextInput/TextView/TextView.Clipboard.cs b/Terminal.Gui/Views/TextInput/TextView/TextView.Clipboard.cs
new file mode 100644
index 0000000000..4a515ac17a
--- /dev/null
+++ b/Terminal.Gui/Views/TextInput/TextView/TextView.Clipboard.cs
@@ -0,0 +1,151 @@
+namespace Terminal.Gui.Views;
+
+public partial class TextView
+{
+ private void SetClipboard (string text)
+ {
+ if (text is { })
+ {
+ Clipboard.Contents = text;
+ }
+ }
+ /// Copy the selected text to the clipboard contents.
+ public void Copy ()
+ {
+ SetWrapModel ();
+
+ if (IsSelecting)
+ {
+ _copiedText = GetRegion (out _copiedCellsList);
+ SetClipboard (_copiedText);
+ _copyWithoutSelection = false;
+ }
+ else
+ {
+ List currentLine = GetCurrentLine ();
+ _copiedCellsList.Add (currentLine);
+ _copiedText = Cell.ToString (currentLine);
+ SetClipboard (_copiedText);
+ _copyWithoutSelection = true;
+ }
+
+ UpdateWrapModel ();
+ DoNeededAction ();
+ }
+
+ /// Cut the selected text to the clipboard contents.
+ public void Cut ()
+ {
+ SetWrapModel ();
+ _copiedText = GetRegion (out _copiedCellsList);
+ SetClipboard (_copiedText);
+
+ if (!_isReadOnly)
+ {
+ ClearRegion ();
+
+ _historyText.Add (
+ [new (GetCurrentLine ())],
+ CursorPosition,
+ TextEditingLineStatus.Replaced
+ );
+ }
+
+ UpdateWrapModel ();
+ IsSelecting = false;
+ DoNeededAction ();
+ OnContentsChanged ();
+ }
+
+ /// Paste the clipboard contents into the current selected position.
+ public void Paste ()
+ {
+ if (_isReadOnly)
+ {
+ return;
+ }
+
+ SetWrapModel ();
+ string? contents = Clipboard.Contents;
+
+ if (_copyWithoutSelection && contents!.FirstOrDefault (x => x is '\n' or '\r') == 0)
+ {
+ List runeList = contents is null ? [] : Cell.ToCellList (contents);
+ List currentLine = GetCurrentLine ();
+
+ _historyText.Add ([new (currentLine)], CursorPosition);
+
+ List> addedLine = [new (currentLine), runeList];
+
+ _historyText.Add (
+ [.. addedLine],
+ CursorPosition,
+ TextEditingLineStatus.Added
+ );
+
+ _model.AddLine (CurrentRow, runeList);
+ CurrentRow++;
+
+ _historyText.Add (
+ [new (GetCurrentLine ())],
+ CursorPosition,
+ TextEditingLineStatus.Replaced
+ );
+
+ SetNeedsDraw ();
+ OnContentsChanged ();
+ }
+ else
+ {
+ if (IsSelecting)
+ {
+ ClearRegion ();
+ }
+
+ _copyWithoutSelection = false;
+ InsertAllText (contents!, true);
+
+ if (IsSelecting)
+ {
+ _historyText.ReplaceLast (
+ [new (GetCurrentLine ())],
+ CursorPosition,
+ TextEditingLineStatus.Original
+ );
+ }
+
+ SetNeedsDraw ();
+ }
+
+ UpdateWrapModel ();
+ IsSelecting = false;
+ DoNeededAction ();
+ }
+
+ private void ProcessCopy ()
+ {
+ ResetColumnTrack ();
+ Copy ();
+ }
+
+ private void ProcessCut ()
+ {
+ ResetColumnTrack ();
+ Cut ();
+ }
+
+ private void ProcessPaste ()
+ {
+ ResetColumnTrack ();
+
+ if (_isReadOnly)
+ {
+ return;
+ }
+
+ Paste ();
+ }
+
+
+ private void AppendClipboard (string text) { Clipboard.Contents += text; }
+}
diff --git a/Terminal.Gui/Views/TextInput/TextView/TextView.ContextMenu.cs b/Terminal.Gui/Views/TextInput/TextView/TextView.ContextMenu.cs
new file mode 100644
index 0000000000..d535c21cbd
--- /dev/null
+++ b/Terminal.Gui/Views/TextInput/TextView/TextView.ContextMenu.cs
@@ -0,0 +1,46 @@
+using System.Globalization;
+
+namespace Terminal.Gui.Views;
+
+/// Context menu functionality
+public partial class TextView
+{
+ private PopoverMenu CreateContextMenu ()
+ {
+ PopoverMenu menu = new (
+ new List
+ {
+ new MenuItem (this, Command.SelectAll, Strings.ctxSelectAll),
+ new MenuItem (this, Command.DeleteAll, Strings.ctxDeleteAll),
+ new MenuItem (this, Command.Copy, Strings.ctxCopy),
+ new MenuItem (this, Command.Cut, Strings.ctxCut),
+ new MenuItem (this, Command.Paste, Strings.ctxPaste),
+ new MenuItem (this, Command.Undo, Strings.ctxUndo),
+ new MenuItem (this, Command.Redo, Strings.ctxRedo)
+ });
+
+ menu.KeyChanged += ContextMenu_KeyChanged;
+
+ return menu;
+ }
+
+ private void ShowContextMenu (Point? mousePosition)
+ {
+ if (!Equals (_currentCulture, Thread.CurrentThread.CurrentUICulture))
+ {
+ _currentCulture = Thread.CurrentThread.CurrentUICulture;
+ }
+
+ if (mousePosition is null)
+ {
+ mousePosition = ViewportToScreen (new Point (CursorPosition.X, CursorPosition.Y));
+ }
+
+ ContextMenu?.MakeVisible (mousePosition);
+ }
+
+ private void ContextMenu_KeyChanged (object? sender, KeyChangedEventArgs e)
+ {
+ KeyBindings.Replace (e.OldKey, e.NewKey);
+ }
+}
diff --git a/Terminal.Gui/Views/TextInput/TextView/TextView.Core.cs b/Terminal.Gui/Views/TextInput/TextView/TextView.Core.cs
new file mode 100644
index 0000000000..b931575097
--- /dev/null
+++ b/Terminal.Gui/Views/TextInput/TextView/TextView.Core.cs
@@ -0,0 +1,649 @@
+using System.Globalization;
+
+namespace Terminal.Gui.Views;
+
+/// Core functionality - Fields, Constructor, and fundamental properties
+public partial class TextView
+{
+ #region Fields
+
+ private readonly HistoryText _historyText = new ();
+ private bool _allowsReturn = true;
+ private bool _allowsTab = true;
+ private bool _clickWithSelecting;
+
+ // The column we are tracking, or -1 if we are not tracking any column
+ private int _columnTrack = -1;
+ private bool _continuousFind;
+ private bool _copyWithoutSelection;
+ private string? _currentCaller;
+ private CultureInfo? _currentCulture;
+ private bool _isButtonShift;
+ private bool _isButtonReleased;
+ private bool _isDrawing;
+ private bool _isReadOnly;
+ private bool _lastWasKill;
+ private int _leftColumn;
+ private TextModel _model = new ();
+ private bool _multiline = true;
+ private Dim? _savedHeight;
+ private int _selectionStartColumn, _selectionStartRow;
+ private bool _shiftSelecting;
+ private int _tabWidth = 4;
+ private int _topRow;
+ private bool _wordWrap;
+ private WordWrapManager? _wrapManager;
+ private bool _wrapNeeded;
+
+ private string? _copiedText;
+ private List> _copiedCellsList = [];
+
+ #endregion
+
+ #region Constructor
+
+ ///
+ /// Initializes a on the specified area, with dimensions controlled with the X, Y, Width
+ /// and Height properties.
+ ///
+ public TextView ()
+ {
+ CanFocus = true;
+ CursorVisibility = CursorVisibility.Default;
+ Used = true;
+
+ // By default, disable hotkeys (in case someone sets Title)
+ base.HotKeySpecifier = new ('\xffff');
+
+ _model.LinesLoaded += Model_LinesLoaded!;
+
+ _historyText.ChangeText += HistoryText_ChangeText!;
+
+ Initialized += TextView_Initialized!;
+
+ SuperViewChanged += TextView_SuperViewChanged!;
+
+ SubViewsLaidOut += TextView_LayoutComplete;
+
+ // Things this view knows how to do
+
+ // Note - NewLine is only bound to Enter if Multiline is true
+ AddCommand (Command.NewLine, ctx => ProcessEnterKey (ctx));
+
+ AddCommand (
+ Command.PageDown,
+ () =>
+ {
+ ProcessPageDown ();
+
+ return true;
+ }
+ );
+
+ AddCommand (
+ Command.PageDownExtend,
+ () =>
+ {
+ ProcessPageDownExtend ();
+
+ return true;
+ }
+ );
+
+ AddCommand (
+ Command.PageUp,
+ () =>
+ {
+ ProcessPageUp ();
+
+ return true;
+ }
+ );
+
+ AddCommand (
+ Command.PageUpExtend,
+ () =>
+ {
+ ProcessPageUpExtend ();
+
+ return true;
+ }
+ );
+
+ AddCommand (Command.Down, () => ProcessMoveDown ());
+
+ AddCommand (
+ Command.DownExtend,
+ () =>
+ {
+ ProcessMoveDownExtend ();
+
+ return true;
+ }
+ );
+
+ AddCommand (Command.Up, () => ProcessMoveUp ());
+
+ AddCommand (
+ Command.UpExtend,
+ () =>
+ {
+ ProcessMoveUpExtend ();
+
+ return true;
+ }
+ );
+ AddCommand (Command.Right, () => ProcessMoveRight ());
+
+ AddCommand (
+ Command.RightExtend,
+ () =>
+ {
+ ProcessMoveRightExtend ();
+
+ return true;
+ }
+ );
+ AddCommand (Command.Left, () => ProcessMoveLeft ());
+
+ AddCommand (
+ Command.LeftExtend,
+ () =>
+ {
+ ProcessMoveLeftExtend ();
+
+ return true;
+ }
+ );
+
+ AddCommand (
+ Command.DeleteCharLeft,
+ () =>
+ {
+ ProcessDeleteCharLeft ();
+
+ return true;
+ }
+ );
+
+ AddCommand (
+ Command.LeftStart,
+ () =>
+ {
+ ProcessMoveLeftStart ();
+
+ return true;
+ }
+ );
+
+ AddCommand (
+ Command.LeftStartExtend,
+ () =>
+ {
+ ProcessMoveLeftStartExtend ();
+
+ return true;
+ }
+ );
+
+ AddCommand (
+ Command.DeleteCharRight,
+ () =>
+ {
+ ProcessDeleteCharRight ();
+
+ return true;
+ }
+ );
+
+ AddCommand (
+ Command.RightEnd,
+ () =>
+ {
+ ProcessMoveEndOfLine ();
+
+ return true;
+ }
+ );
+
+ AddCommand (
+ Command.RightEndExtend,
+ () =>
+ {
+ ProcessMoveRightEndExtend ();
+
+ return true;
+ }
+ );
+
+ AddCommand (
+ Command.CutToEndLine,
+ () =>
+ {
+ KillToEndOfLine ();
+
+ return true;
+ }
+ );
+
+ AddCommand (
+ Command.CutToStartLine,
+ () =>
+ {
+ KillToLeftStart ();
+
+ return true;
+ }
+ );
+
+ AddCommand (
+ Command.Paste,
+ () =>
+ {
+ ProcessPaste ();
+
+ return true;
+ }
+ );
+
+ AddCommand (
+ Command.ToggleExtend,
+ () =>
+ {
+ ToggleSelecting ();
+
+ return true;
+ }
+ );
+
+ AddCommand (
+ Command.Copy,
+ () =>
+ {
+ ProcessCopy ();
+
+ return true;
+ }
+ );
+
+ AddCommand (
+ Command.Cut,
+ () =>
+ {
+ ProcessCut ();
+
+ return true;
+ }
+ );
+
+ AddCommand (
+ Command.WordLeft,
+ () =>
+ {
+ ProcessMoveWordBackward ();
+
+ return true;
+ }
+ );
+
+ AddCommand (
+ Command.WordLeftExtend,
+ () =>
+ {
+ ProcessMoveWordBackwardExtend ();
+
+ return true;
+ }
+ );
+
+ AddCommand (
+ Command.WordRight,
+ () =>
+ {
+ ProcessMoveWordForward ();
+
+ return true;
+ }
+ );
+
+ AddCommand (
+ Command.WordRightExtend,
+ () =>
+ {
+ ProcessMoveWordForwardExtend ();
+
+ return true;
+ }
+ );
+
+ AddCommand (
+ Command.KillWordForwards,
+ () =>
+ {
+ ProcessKillWordForward ();
+
+ return true;
+ }
+ );
+
+ AddCommand (
+ Command.KillWordBackwards,
+ () =>
+ {
+ ProcessKillWordBackward ();
+
+ return true;
+ }
+ );
+
+ AddCommand (
+ Command.End,
+ () =>
+ {
+ MoveBottomEnd ();
+
+ return true;
+ }
+ );
+
+ AddCommand (
+ Command.EndExtend,
+ () =>
+ {
+ MoveBottomEndExtend ();
+
+ return true;
+ }
+ );
+
+ AddCommand (
+ Command.Start,
+ () =>
+ {
+ MoveTopHome ();
+
+ return true;
+ }
+ );
+
+ AddCommand (
+ Command.StartExtend,
+ () =>
+ {
+ MoveTopHomeExtend ();
+
+ return true;
+ }
+ );
+
+ AddCommand (
+ Command.SelectAll,
+ () =>
+ {
+ ProcessSelectAll ();
+
+ return true;
+ }
+ );
+
+ AddCommand (
+ Command.ToggleOverwrite,
+ () =>
+ {
+ ProcessSetOverwrite ();
+
+ return true;
+ }
+ );
+
+ AddCommand (
+ Command.EnableOverwrite,
+ () =>
+ {
+ SetOverwrite (true);
+
+ return true;
+ }
+ );
+
+ AddCommand (
+ Command.DisableOverwrite,
+ () =>
+ {
+ SetOverwrite (false);
+
+ return true;
+ }
+ );
+ AddCommand (Command.Tab, () => ProcessTab ());
+ AddCommand (Command.BackTab, () => ProcessBackTab ());
+
+ AddCommand (
+ Command.Undo,
+ () =>
+ {
+ Undo ();
+
+ return true;
+ }
+ );
+
+ AddCommand (
+ Command.Redo,
+ () =>
+ {
+ Redo ();
+
+ return true;
+ }
+ );
+
+ AddCommand (
+ Command.DeleteAll,
+ () =>
+ {
+ DeleteAll ();
+
+ return true;
+ }
+ );
+
+ AddCommand (
+ Command.Context,
+ () =>
+ {
+ ShowContextMenu (null);
+
+ return true;
+ }
+ );
+
+ AddCommand (
+ Command.Open,
+ () =>
+ {
+ PromptForColors ();
+
+ return true;
+ });
+
+ // Default keybindings for this view
+ KeyBindings.Remove (Key.Space);
+
+ KeyBindings.Remove (Key.Enter);
+ KeyBindings.Add (Key.Enter, Multiline ? Command.NewLine : Command.Accept);
+
+ KeyBindings.Add (Key.PageDown, Command.PageDown);
+ KeyBindings.Add (Key.V.WithCtrl, Command.PageDown);
+
+ KeyBindings.Add (Key.PageDown.WithShift, Command.PageDownExtend);
+
+ KeyBindings.Add (Key.PageUp, Command.PageUp);
+
+ KeyBindings.Add (Key.PageUp.WithShift, Command.PageUpExtend);
+
+ KeyBindings.Add (Key.N.WithCtrl, Command.Down);
+ KeyBindings.Add (Key.CursorDown, Command.Down);
+
+ KeyBindings.Add (Key.CursorDown.WithShift, Command.DownExtend);
+
+ KeyBindings.Add (Key.P.WithCtrl, Command.Up);
+ KeyBindings.Add (Key.CursorUp, Command.Up);
+
+ KeyBindings.Add (Key.CursorUp.WithShift, Command.UpExtend);
+
+ KeyBindings.Add (Key.F.WithCtrl, Command.Right);
+ KeyBindings.Add (Key.CursorRight, Command.Right);
+
+ KeyBindings.Add (Key.CursorRight.WithShift, Command.RightExtend);
+
+ KeyBindings.Add (Key.B.WithCtrl, Command.Left);
+ KeyBindings.Add (Key.CursorLeft, Command.Left);
+
+ KeyBindings.Add (Key.CursorLeft.WithShift, Command.LeftExtend);
+
+ KeyBindings.Add (Key.Backspace, Command.DeleteCharLeft);
+
+ KeyBindings.Add (Key.Home, Command.LeftStart);
+
+ KeyBindings.Add (Key.Home.WithShift, Command.LeftStartExtend);
+
+ KeyBindings.Add (Key.Delete, Command.DeleteCharRight);
+ KeyBindings.Add (Key.D.WithCtrl, Command.DeleteCharRight);
+
+ KeyBindings.Add (Key.End, Command.RightEnd);
+ KeyBindings.Add (Key.E.WithCtrl, Command.RightEnd);
+
+ KeyBindings.Add (Key.End.WithShift, Command.RightEndExtend);
+
+ KeyBindings.Add (Key.K.WithCtrl, Command.CutToEndLine); // kill-to-end
+
+ KeyBindings.Add (Key.Delete.WithCtrl.WithShift, Command.CutToEndLine); // kill-to-end
+
+ KeyBindings.Add (Key.Backspace.WithCtrl.WithShift, Command.CutToStartLine); // kill-to-start
+
+ KeyBindings.Add (Key.Y.WithCtrl, Command.Paste); // Control-y, yank
+ KeyBindings.Add (Key.Space.WithCtrl, Command.ToggleExtend);
+
+ KeyBindings.Add (Key.C.WithCtrl, Command.Copy);
+
+ KeyBindings.Add (Key.W.WithCtrl, Command.Cut); // Move to Unix?
+ KeyBindings.Add (Key.X.WithCtrl, Command.Cut);
+
+ KeyBindings.Add (Key.CursorLeft.WithCtrl, Command.WordLeft);
+
+ KeyBindings.Add (Key.CursorLeft.WithCtrl.WithShift, Command.WordLeftExtend);
+
+ KeyBindings.Add (Key.CursorRight.WithCtrl, Command.WordRight);
+
+ KeyBindings.Add (Key.CursorRight.WithCtrl.WithShift, Command.WordRightExtend);
+ KeyBindings.Add (Key.Delete.WithCtrl, Command.KillWordForwards); // kill-word-forwards
+ KeyBindings.Add (Key.Backspace.WithCtrl, Command.KillWordBackwards); // kill-word-backwards
+
+ KeyBindings.Add (Key.End.WithCtrl, Command.End);
+ KeyBindings.Add (Key.End.WithCtrl.WithShift, Command.EndExtend);
+ KeyBindings.Add (Key.Home.WithCtrl, Command.Start);
+ KeyBindings.Add (Key.Home.WithCtrl.WithShift, Command.StartExtend);
+ KeyBindings.Add (Key.A.WithCtrl, Command.SelectAll);
+ KeyBindings.Add (Key.InsertChar, Command.ToggleOverwrite);
+ KeyBindings.Add (Key.Tab, Command.Tab);
+ KeyBindings.Add (Key.Tab.WithShift, Command.BackTab);
+
+ KeyBindings.Add (Key.Z.WithCtrl, Command.Undo);
+ KeyBindings.Add (Key.R.WithCtrl, Command.Redo);
+
+ KeyBindings.Add (Key.G.WithCtrl, Command.DeleteAll);
+ KeyBindings.Add (Key.D.WithCtrl.WithShift, Command.DeleteAll);
+
+ KeyBindings.Add (Key.L.WithCtrl, Command.Open);
+
+#if UNIX_KEY_BINDINGS
+ KeyBindings.Add (Key.C.WithAlt, Command.Copy);
+ KeyBindings.Add (Key.B.WithAlt, Command.WordLeft);
+ KeyBindings.Add (Key.W.WithAlt, Command.Cut);
+ KeyBindings.Add (Key.V.WithAlt, Command.PageUp);
+ KeyBindings.Add (Key.F.WithAlt, Command.WordRight);
+ KeyBindings.Add (Key.K.WithAlt, Command.CutToStartLine); // kill-to-start
+#endif
+
+ _currentCulture = Thread.CurrentThread.CurrentUICulture;
+ }
+
+ #endregion
+
+ #region Initialization and Configuration
+
+
+ private void TextView_Initialized (object sender, EventArgs e)
+ {
+ if (Autocomplete.HostControl is null)
+ {
+ Autocomplete.HostControl = this;
+ }
+
+ ContextMenu = CreateContextMenu ();
+ App?.Popover?.Register (ContextMenu);
+ KeyBindings.Add (ContextMenu.Key, Command.Context);
+
+ // Configure ScrollBars to use modern View scrolling infrastructure
+ ConfigureLayout ();
+
+ OnContentsChanged ();
+ }
+
+ private void TextView_SuperViewChanged (object sender, SuperViewChangedEventArgs e)
+ {
+ if (e.SuperView is { })
+ {
+ if (Autocomplete.HostControl is null)
+ {
+ Autocomplete.HostControl = this;
+ }
+ }
+ else
+ {
+ Autocomplete.HostControl = null;
+ }
+ }
+
+ private void Model_LinesLoaded (object sender, EventArgs e)
+ {
+ // This call is not needed. Model_LinesLoaded gets invoked when
+ // model.LoadString (value) is called. LoadString is called from one place
+ // (Text.set) and historyText.Clear() is called immediately after.
+ // If this call happens, HistoryText_ChangeText will get called multiple times
+ // when Text is set, which is wrong.
+ //historyText.Clear (Text);
+
+ if (!_multiline && !IsInitialized)
+ {
+ CurrentColumn = Text.GetRuneCount ();
+ _leftColumn = CurrentColumn > Viewport.Width + 1 ? CurrentColumn - Viewport.Width + 1 : 0;
+ }
+ }
+
+ #endregion
+
+ ///
+ /// INTERNAL: Determines if a redraw is needed based on selection state, word wrap needs, and Used flag.
+ /// If a redraw is needed, calls ; otherwise positions the cursor and updates
+ /// the unwrapped cursor position.
+ ///
+ private void DoNeededAction ()
+ {
+ if (!NeedsDraw && (IsSelecting || _wrapNeeded || !Used))
+ {
+ SetNeedsDraw ();
+ }
+
+ if (NeedsDraw)
+ {
+ AdjustScrollPosition ();
+ }
+ else
+ {
+ PositionCursor ();
+ OnUnwrappedCursorPosition ();
+ }
+ }
+}
diff --git a/Terminal.Gui/Views/TextInput/TextView/TextView.Delete.cs b/Terminal.Gui/Views/TextInput/TextView/TextView.Delete.cs
new file mode 100644
index 0000000000..1c982ff141
--- /dev/null
+++ b/Terminal.Gui/Views/TextInput/TextView/TextView.Delete.cs
@@ -0,0 +1,654 @@
+namespace Terminal.Gui.Views;
+
+public partial class TextView
+{
+ /// Deletes all text.
+ public void DeleteAll ()
+ {
+ if (Lines == 0)
+ {
+ return;
+ }
+
+ _selectionStartColumn = 0;
+ _selectionStartRow = 0;
+ MoveBottomEndExtend ();
+ DeleteCharLeft ();
+ SetNeedsDraw ();
+ }
+
+ /// Deletes all the selected or a single character at left from the position of the cursor.
+ public void DeleteCharLeft ()
+ {
+ if (_isReadOnly)
+ {
+ return;
+ }
+
+ SetWrapModel ();
+
+ if (IsSelecting)
+ {
+ _historyText.Add (new () { new (GetCurrentLine ()) }, CursorPosition);
+
+ ClearSelectedRegion ();
+
+ List currentLine = GetCurrentLine ();
+
+ _historyText.Add (
+ new () { new (currentLine) },
+ CursorPosition,
+ TextEditingLineStatus.Replaced
+ );
+
+ UpdateWrapModel ();
+ OnContentsChanged ();
+
+ return;
+ }
+
+ if (DeleteTextBackwards ())
+ {
+ UpdateWrapModel ();
+ OnContentsChanged ();
+
+ return;
+ }
+
+ UpdateWrapModel ();
+
+ DoNeededAction ();
+ OnContentsChanged ();
+ }
+
+ /// Deletes all the selected or a single character at right from the position of the cursor.
+ public void DeleteCharRight ()
+ {
+ if (_isReadOnly)
+ {
+ return;
+ }
+
+ SetWrapModel ();
+
+ if (IsSelecting)
+ {
+ _historyText.Add (new () { new (GetCurrentLine ()) }, CursorPosition);
+
+ ClearSelectedRegion ();
+
+ List currentLine = GetCurrentLine ();
+
+ _historyText.Add (
+ new () { new (currentLine) },
+ CursorPosition,
+ TextEditingLineStatus.Replaced
+ );
+
+ UpdateWrapModel ();
+ OnContentsChanged ();
+
+ return;
+ }
+
+ if (DeleteTextForwards ())
+ {
+ UpdateWrapModel ();
+ OnContentsChanged ();
+
+ return;
+ }
+
+ UpdateWrapModel ();
+
+ DoNeededAction ();
+ OnContentsChanged ();
+ }
+
+ private bool DeleteTextBackwards ()
+ {
+ SetWrapModel ();
+
+ if (CurrentColumn > 0)
+ {
+ // Delete backwards
+ List currentLine = GetCurrentLine ();
+
+ _historyText.Add (new () { new (currentLine) }, CursorPosition);
+
+ currentLine.RemoveAt (CurrentColumn - 1);
+
+ if (_wordWrap)
+ {
+ _wrapNeeded = true;
+ }
+
+ CurrentColumn--;
+
+ _historyText.Add (
+ new () { new (currentLine) },
+ CursorPosition,
+ TextEditingLineStatus.Replaced
+ );
+
+ if (CurrentColumn < _leftColumn)
+ {
+ _leftColumn--;
+ SetNeedsDraw ();
+ }
+ else
+ {
+ // BUGBUG: customized rect aren't supported now because the Redraw isn't using the Intersect method.
+ //SetNeedsDraw (new (0, currentRow - topRow, 1, Viewport.Width));
+ SetNeedsDraw ();
+ }
+ }
+ else
+ {
+ // Merges the current line with the previous one.
+ if (CurrentRow == 0)
+ {
+ return true;
+ }
+
+ int prowIdx = CurrentRow - 1;
+ List prevRow = _model.GetLine (prowIdx);
+
+ _historyText.Add (new () { new (prevRow) }, CursorPosition);
+
+ List> removedLines = new () { new (prevRow) };
+
+ removedLines.Add (new (GetCurrentLine ()));
+
+ _historyText.Add (
+ removedLines,
+ new (CurrentColumn, prowIdx),
+ TextEditingLineStatus.Removed
+ );
+
+ int prevCount = prevRow.Count;
+ _model.GetLine (prowIdx).AddRange (GetCurrentLine ());
+ _model.RemoveLine (CurrentRow);
+
+ if (_wordWrap)
+ {
+ _wrapNeeded = true;
+ }
+
+ CurrentRow--;
+
+ _historyText.Add (
+ new () { GetCurrentLine () },
+ new (CurrentColumn, prowIdx),
+ TextEditingLineStatus.Replaced
+ );
+
+ CurrentColumn = prevCount;
+ SetNeedsDraw ();
+ }
+
+ UpdateWrapModel ();
+
+ return false;
+ }
+
+ private bool DeleteTextForwards ()
+ {
+ SetWrapModel ();
+
+ List currentLine = GetCurrentLine ();
+
+ if (CurrentColumn == currentLine.Count)
+ {
+ if (CurrentRow + 1 == _model.Count)
+ {
+ UpdateWrapModel ();
+
+ return true;
+ }
+
+ _historyText.Add (new () { new (currentLine) }, CursorPosition);
+
+ List> removedLines = new () { new (currentLine) };
+
+ List nextLine = _model.GetLine (CurrentRow + 1);
+
+ removedLines.Add (new (nextLine));
+
+ _historyText.Add (removedLines, CursorPosition, TextEditingLineStatus.Removed);
+
+ currentLine.AddRange (nextLine);
+ _model.RemoveLine (CurrentRow + 1);
+
+ _historyText.Add (
+ new () { new (currentLine) },
+ CursorPosition,
+ TextEditingLineStatus.Replaced
+ );
+
+ if (_wordWrap)
+ {
+ _wrapNeeded = true;
+ }
+
+ DoSetNeedsDraw (new (0, CurrentRow - _topRow, Viewport.Width, CurrentRow - _topRow + 1));
+ }
+ else
+ {
+ _historyText.Add ([ [.. currentLine]], CursorPosition);
+
+ currentLine.RemoveAt (CurrentColumn);
+
+ _historyText.Add (
+ [ [.. currentLine]],
+ CursorPosition,
+ TextEditingLineStatus.Replaced
+ );
+
+ if (_wordWrap)
+ {
+ _wrapNeeded = true;
+ }
+
+ DoSetNeedsDraw (
+ new (
+ CurrentColumn - _leftColumn,
+ CurrentRow - _topRow,
+ Viewport.Width,
+ Math.Max (CurrentRow - _topRow + 1, 0)
+ )
+ );
+ }
+
+ UpdateWrapModel ();
+
+ return false;
+ }
+
+ private void ProcessKillWordForward ()
+ {
+ ResetColumnTrack ();
+ StopSelecting ();
+ KillWordForward ();
+ }
+
+ private void ProcessKillWordBackward ()
+ {
+ ResetColumnTrack ();
+ KillWordBackward ();
+ }
+
+ private void ProcessDeleteCharRight ()
+ {
+ ResetColumnTrack ();
+ DeleteCharRight ();
+ }
+
+ private void ProcessDeleteCharLeft ()
+ {
+ ResetColumnTrack ();
+ DeleteCharLeft ();
+ }
+
+ private void KillWordForward ()
+ {
+ if (_isReadOnly)
+ {
+ return;
+ }
+
+ SetWrapModel ();
+
+ List currentLine = GetCurrentLine ();
+
+ _historyText.Add ([ [.. GetCurrentLine ()]], CursorPosition);
+
+ if (currentLine.Count == 0 || CurrentColumn == currentLine.Count)
+ {
+ DeleteTextForwards ();
+
+ _historyText.ReplaceLast (
+ [ [.. GetCurrentLine ()]],
+ CursorPosition,
+ TextEditingLineStatus.Replaced
+ );
+
+ UpdateWrapModel ();
+
+ return;
+ }
+
+ (int col, int row)? newPos = _model.WordForward (CurrentColumn, CurrentRow, UseSameRuneTypeForWords);
+ var restCount = 0;
+
+ if (newPos.HasValue && CurrentRow == newPos.Value.row)
+ {
+ restCount = newPos.Value.col - CurrentColumn;
+ currentLine.RemoveRange (CurrentColumn, restCount);
+ }
+ else if (newPos.HasValue)
+ {
+ restCount = currentLine.Count - CurrentColumn;
+ currentLine.RemoveRange (CurrentColumn, restCount);
+ }
+
+ if (_wordWrap)
+ {
+ _wrapNeeded = true;
+ }
+
+ _historyText.Add (
+ [ [.. GetCurrentLine ()]],
+ CursorPosition,
+ TextEditingLineStatus.Replaced
+ );
+
+ UpdateWrapModel ();
+
+ DoSetNeedsDraw (new (0, CurrentRow - _topRow, Viewport.Width, Viewport.Height));
+ DoNeededAction ();
+ }
+
+ private void KillWordBackward ()
+ {
+ if (_isReadOnly)
+ {
+ return;
+ }
+
+ SetWrapModel ();
+
+ List currentLine = GetCurrentLine ();
+
+ _historyText.Add ([ [.. GetCurrentLine ()]], CursorPosition);
+
+ if (CurrentColumn == 0)
+ {
+ DeleteTextBackwards ();
+
+ _historyText.ReplaceLast (
+ [ [.. GetCurrentLine ()]],
+ CursorPosition,
+ TextEditingLineStatus.Replaced
+ );
+
+ UpdateWrapModel ();
+
+ return;
+ }
+
+ (int col, int row)? newPos = _model.WordBackward (CurrentColumn, CurrentRow, UseSameRuneTypeForWords);
+
+ if (newPos.HasValue && CurrentRow == newPos.Value.row)
+ {
+ int restCount = CurrentColumn - newPos.Value.col;
+ currentLine.RemoveRange (newPos.Value.col, restCount);
+
+ if (_wordWrap)
+ {
+ _wrapNeeded = true;
+ }
+
+ CurrentColumn = newPos.Value.col;
+ }
+ else if (newPos.HasValue)
+ {
+ int restCount;
+
+ if (newPos.Value.row == CurrentRow)
+ {
+ restCount = currentLine.Count - CurrentColumn;
+ currentLine.RemoveRange (CurrentColumn, restCount);
+ }
+ else
+ {
+ while (CurrentRow != newPos.Value.row)
+ {
+ restCount = currentLine.Count;
+ currentLine.RemoveRange (0, restCount);
+
+ CurrentRow--;
+ currentLine = GetCurrentLine ();
+ }
+ }
+
+ if (_wordWrap)
+ {
+ _wrapNeeded = true;
+ }
+
+ CurrentColumn = newPos.Value.col;
+ CurrentRow = newPos.Value.row;
+ }
+
+ _historyText.Add (
+ [ [.. GetCurrentLine ()]],
+ CursorPosition,
+ TextEditingLineStatus.Replaced
+ );
+
+ UpdateWrapModel ();
+
+ DoSetNeedsDraw (new (0, CurrentRow - _topRow, Viewport.Width, Viewport.Height));
+ DoNeededAction ();
+ }
+
+ private void KillToLeftStart ()
+ {
+ if (_isReadOnly)
+ {
+ return;
+ }
+
+ if (_model.Count == 1 && GetCurrentLine ().Count == 0)
+ {
+ // Prevents from adding line feeds if there is no more lines.
+ return;
+ }
+
+ SetWrapModel ();
+
+ List currentLine = GetCurrentLine ();
+ var setLastWasKill = true;
+
+ if (currentLine.Count > 0 && CurrentColumn == 0)
+ {
+ UpdateWrapModel ();
+
+ DeleteTextBackwards ();
+
+ return;
+ }
+
+ _historyText.Add ([ [.. currentLine]], CursorPosition);
+
+ if (currentLine.Count == 0)
+ {
+ if (CurrentRow > 0)
+ {
+ _model.RemoveLine (CurrentRow);
+
+ if (_model.Count > 0 || _lastWasKill)
+ {
+ string val = Environment.NewLine;
+
+ if (_lastWasKill)
+ {
+ AppendClipboard (val);
+ }
+ else
+ {
+ SetClipboard (val);
+ }
+ }
+
+ if (_model.Count == 0)
+ {
+ // Prevents from adding line feeds if there is no more lines.
+ setLastWasKill = false;
+ }
+
+ CurrentRow--;
+ currentLine = _model.GetLine (CurrentRow);
+
+ List> removedLine =
+ [
+ [..currentLine],
+ []
+ ];
+
+ _historyText.Add (
+ [.. removedLine],
+ CursorPosition,
+ TextEditingLineStatus.Removed
+ );
+
+ CurrentColumn = currentLine.Count;
+ }
+ }
+ else
+ {
+ int restCount = CurrentColumn;
+ List rest = currentLine.GetRange (0, restCount);
+ var val = string.Empty;
+ val += StringFromCells (rest);
+
+ if (_lastWasKill)
+ {
+ AppendClipboard (val);
+ }
+ else
+ {
+ SetClipboard (val);
+ }
+
+ currentLine.RemoveRange (0, restCount);
+ CurrentColumn = 0;
+ }
+
+ _historyText.Add (
+ [ [.. GetCurrentLine ()]],
+ CursorPosition,
+ TextEditingLineStatus.Replaced
+ );
+
+ UpdateWrapModel ();
+
+ DoSetNeedsDraw (new (0, CurrentRow - _topRow, Viewport.Width, Viewport.Height));
+
+ _lastWasKill = setLastWasKill;
+ DoNeededAction ();
+ }
+
+ private void KillToEndOfLine ()
+ {
+ if (_isReadOnly)
+ {
+ return;
+ }
+
+ if (_model.Count == 1 && GetCurrentLine ().Count == 0)
+ {
+ // Prevents from adding line feeds if there is no more lines.
+ return;
+ }
+
+ SetWrapModel ();
+
+ List currentLine = GetCurrentLine ();
+ var setLastWasKill = true;
+
+ if (currentLine.Count > 0 && CurrentColumn == currentLine.Count)
+ {
+ UpdateWrapModel ();
+
+ DeleteTextForwards ();
+
+ return;
+ }
+
+ _historyText.Add (new () { new (currentLine) }, CursorPosition);
+
+ if (currentLine.Count == 0)
+ {
+ if (CurrentRow < _model.Count - 1)
+ {
+ List> removedLines = new () { new (currentLine) };
+
+ _model.RemoveLine (CurrentRow);
+
+ removedLines.Add (new (GetCurrentLine ()));
+
+ _historyText.Add (
+ new (removedLines),
+ CursorPosition,
+ TextEditingLineStatus.Removed
+ );
+ }
+
+ if (_model.Count > 0 || _lastWasKill)
+ {
+ string val = Environment.NewLine;
+
+ if (_lastWasKill)
+ {
+ AppendClipboard (val);
+ }
+ else
+ {
+ SetClipboard (val);
+ }
+ }
+
+ if (_model.Count == 0)
+ {
+ // Prevents from adding line feeds if there is no more lines.
+ setLastWasKill = false;
+ }
+ }
+ else
+ {
+ int restCount = currentLine.Count - CurrentColumn;
+ List rest = currentLine.GetRange (CurrentColumn, restCount);
+ var val = string.Empty;
+ val += StringFromCells (rest);
+
+ if (_lastWasKill)
+ {
+ AppendClipboard (val);
+ }
+ else
+ {
+ SetClipboard (val);
+ }
+
+ currentLine.RemoveRange (CurrentColumn, restCount);
+ }
+
+ _historyText.Add (
+ [ [.. GetCurrentLine ()]],
+ CursorPosition,
+ TextEditingLineStatus.Replaced
+ );
+
+ UpdateWrapModel ();
+
+ DoSetNeedsDraw (new (0, CurrentRow - _topRow, Viewport.Width, Viewport.Height));
+
+ _lastWasKill = setLastWasKill;
+ DoNeededAction ();
+ }
+
+ ///
+ /// INTERNAL: Resets the column tracking state and last kill operation flag.
+ /// Column tracking is used to maintain the desired cursor column position when moving up/down
+ /// through lines of different lengths.
+ ///
+ private void ResetColumnTrack ()
+ {
+ // Handle some state here - whether the last command was a kill
+ // operation and the column tracking (up/down)
+ _lastWasKill = false;
+ _columnTrack = -1;
+ }
+}
diff --git a/Terminal.Gui/Views/TextInput/TextView/TextView.Drawing.cs b/Terminal.Gui/Views/TextInput/TextView/TextView.Drawing.cs
new file mode 100644
index 0000000000..99950bdf38
--- /dev/null
+++ b/Terminal.Gui/Views/TextInput/TextView/TextView.Drawing.cs
@@ -0,0 +1,348 @@
+namespace Terminal.Gui.Views;
+
+public partial class TextView
+{
+ internal void ApplyCellsAttribute (Attribute attribute)
+ {
+ if (!ReadOnly && SelectedLength > 0)
+ {
+ int startRow = Math.Min (SelectionStartRow, CurrentRow);
+ int endRow = Math.Max (CurrentRow, SelectionStartRow);
+ int startCol = SelectionStartRow <= CurrentRow ? SelectionStartColumn : CurrentColumn;
+ int endCol = CurrentRow >= SelectionStartRow ? CurrentColumn : SelectionStartColumn;
+ List> selectedCellsOriginal = [];
+ List> selectedCellsChanged = [];
+
+ for (int r = startRow; r <= endRow; r++)
+ {
+ List line = GetLine (r);
+
+ selectedCellsOriginal.Add ([.. line]);
+
+ for (int c = r == startRow ? startCol : 0;
+ c < (r == endRow ? endCol : line.Count);
+ c++)
+ {
+ Cell cell = line [c]; // Copy value to a new variable
+ cell.Attribute = attribute; // Modify the copy
+ line [c] = cell; // Assign the modified copy back
+ }
+
+ selectedCellsChanged.Add ([.. GetLine (r)]);
+ }
+
+ GetSelectedRegion ();
+ IsSelecting = false;
+
+ _historyText.Add (
+ [.. selectedCellsOriginal],
+ new Point (startCol, startRow)
+ );
+
+ _historyText.Add (
+ [.. selectedCellsChanged],
+ new Point (startCol, startRow),
+ TextEditingLineStatus.Attribute
+ );
+ }
+ }
+
+ private Attribute? GetSelectedCellAttribute ()
+ {
+ List line;
+
+ if (SelectedLength > 0)
+ {
+ line = GetLine (SelectionStartRow);
+
+ if (line [Math.Min (SelectionStartColumn, line.Count - 1)].Attribute is { } attributeSel)
+ {
+ return new (attributeSel);
+ }
+
+ return GetAttributeForRole (VisualRole.Active);
+ }
+
+ line = GetCurrentLine ();
+
+ if (line [Math.Min (CurrentColumn, line.Count - 1)].Attribute is { } attribute)
+ {
+ return new (attribute);
+ }
+
+ return GetAttributeForRole (VisualRole.Active);
+ }
+
+ /// Invoked when the normal color is drawn.
+ public event EventHandler? DrawNormalColor;
+
+ /// Invoked when the ready only color is drawn.
+ public event EventHandler? DrawReadOnlyColor;
+
+ /// Invoked when the selection color is drawn.
+ public event EventHandler? DrawSelectionColor;
+
+ ///
+ /// Invoked when the used color is drawn. The Used Color is used to indicate if the
+ /// was pressed and enabled.
+ ///
+ public event EventHandler? DrawUsedColor;
+
+ ///
+ protected override bool OnDrawingContent ()
+ {
+ _isDrawing = true;
+
+ SetAttributeForRole (Enabled ? VisualRole.Editable : VisualRole.Disabled);
+
+ (int width, int height) offB = GetViewportClipping ();
+ int right = Viewport.Width + offB.width;
+ int bottom = Viewport.Height + offB.height;
+ var row = 0;
+
+ for (int idxRow = _topRow; idxRow < _model.Count; idxRow++)
+ {
+ List line = _model.GetLine (idxRow);
+ int lineRuneCount = line.Count;
+ var col = 0;
+
+ Move (0, row);
+
+ for (int idxCol = _leftColumn; idxCol < lineRuneCount; idxCol++)
+ {
+ string text = idxCol >= lineRuneCount ? " " : line [idxCol].Grapheme;
+ int cols = text.GetColumns (false);
+
+ if (idxCol < line.Count && IsSelecting && PointInSelection (idxCol, idxRow))
+ {
+ OnDrawSelectionColor (line, idxCol, idxRow);
+ }
+ else if (idxCol == CurrentColumn && idxRow == CurrentRow && !IsSelecting && !Used && HasFocus && idxCol < lineRuneCount)
+ {
+ OnDrawUsedColor (line, idxCol, idxRow);
+ }
+ else if (ReadOnly)
+ {
+ OnDrawReadOnlyColor (line, idxCol, idxRow);
+ }
+ else
+ {
+ OnDrawNormalColor (line, idxCol, idxRow);
+ }
+
+ if (text == "\t")
+ {
+ cols += TabWidth + 1;
+
+ if (col + cols > right)
+ {
+ cols = right - col;
+ }
+
+ for (var i = 0; i < cols; i++)
+ {
+ if (col + i < right)
+ {
+ AddRune (col + i, row, (Rune)' ');
+ }
+ }
+ }
+ else
+ {
+ AddStr (col, row, text);
+
+ // Ensures that cols less than 0 to be 1 because it will be converted to a printable rune
+ cols = Math.Max (cols, 1);
+ }
+
+ if (!TextModel.SetCol (ref col, Viewport.Right, cols))
+ {
+ break;
+ }
+
+ if (idxCol + 1 < lineRuneCount && col + line [idxCol + 1].Grapheme.GetColumns () > right)
+ {
+ break;
+ }
+ }
+
+ if (col < right)
+ {
+ SetAttributeForRole (ReadOnly ? VisualRole.ReadOnly : VisualRole.Editable);
+ ClearRegion (col, row, right, row + 1);
+ }
+
+ row++;
+ }
+
+ if (row < bottom)
+ {
+ SetAttributeForRole (ReadOnly ? VisualRole.ReadOnly : VisualRole.Editable);
+ ClearRegion (Viewport.Left, row, right, bottom);
+ }
+
+ _isDrawing = false;
+
+ return false;
+ }
+
+ ///
+ /// Sets the to an appropriate color for rendering the given
+ /// of the current . Override to provide custom coloring by calling
+ /// Defaults to .
+ ///
+ /// The line.
+ /// The col index.
+ /// The row index.
+ protected virtual void OnDrawNormalColor (List line, int idxCol, int idxRow)
+ {
+ (int Row, int Col) unwrappedPos = GetUnwrappedPosition (idxRow, idxCol);
+ var ev = new CellEventArgs (line, idxCol, unwrappedPos);
+ DrawNormalColor?.Invoke (this, ev);
+
+ if (line [idxCol].Attribute is { })
+ {
+ Attribute? attribute = line [idxCol].Attribute;
+ SetAttribute ((Attribute)attribute!);
+ }
+ else
+ {
+ SetAttribute (GetAttributeForRole (VisualRole.Normal));
+ }
+ }
+
+ ///
+ /// Sets the to an appropriate color for rendering the given
+ /// of the current . Override to provide custom coloring by calling
+ /// Defaults to .
+ ///
+ /// The line.
+ /// The col index.
+ /// ///
+ /// The row index.
+ protected virtual void OnDrawReadOnlyColor (List line, int idxCol, int idxRow)
+ {
+ (int Row, int Col) unwrappedPos = GetUnwrappedPosition (idxRow, idxCol);
+ var ev = new CellEventArgs (line, idxCol, unwrappedPos);
+ DrawReadOnlyColor?.Invoke (this, ev);
+
+ Attribute? cellAttribute = line [idxCol].Attribute is { } ? line [idxCol].Attribute : GetAttributeForRole (VisualRole.ReadOnly);
+
+ if (cellAttribute!.Value.Foreground == cellAttribute.Value.Background)
+ {
+ SetAttribute (new (cellAttribute.Value.Foreground, cellAttribute.Value.Background, cellAttribute.Value.Style));
+ }
+ else
+ {
+ SetAttributeForRole (VisualRole.ReadOnly);
+ }
+ }
+
+ ///
+ /// Sets the to an appropriate color for rendering the given
+ /// of the current . Override to provide custom coloring by calling
+ /// Defaults to .
+ ///
+ /// The line.
+ /// The col index.
+ /// ///
+ /// The row index.
+ protected virtual void OnDrawSelectionColor (List line, int idxCol, int idxRow)
+ {
+ (int Row, int Col) unwrappedPos = GetUnwrappedPosition (idxRow, idxCol);
+ var ev = new CellEventArgs (line, idxCol, unwrappedPos);
+ DrawSelectionColor?.Invoke (this, ev);
+
+ if (line [idxCol].Attribute is { })
+ {
+ Attribute? attribute = line [idxCol].Attribute;
+ Attribute? active = GetAttributeForRole (VisualRole.Active);
+ SetAttribute (new (active!.Value.Foreground, active.Value.Background, attribute!.Value.Style));
+ }
+ else
+ {
+ SetAttributeForRole (VisualRole.Active);
+ }
+ }
+
+ ///
+ /// Sets the to an appropriate color for rendering the given
+ /// of the current . Override to provide custom coloring by calling
+ /// Defaults to .
+ ///
+ /// The line.
+ /// The col index.
+ /// ///
+ /// The row index.
+ protected virtual void OnDrawUsedColor (List line, int idxCol, int idxRow)
+ {
+ (int Row, int Col) unwrappedPos = GetUnwrappedPosition (idxRow, idxCol);
+ var ev = new CellEventArgs (line, idxCol, unwrappedPos);
+ DrawUsedColor?.Invoke (this, ev);
+
+ if (line [idxCol].Attribute is { })
+ {
+ Attribute? attribute = line [idxCol].Attribute;
+ SetValidUsedColor (attribute!);
+ }
+ else
+ {
+ SetValidUsedColor (GetAttributeForRole (VisualRole.Focus));
+ }
+ }
+
+ private void DoSetNeedsDraw (Rectangle rect)
+ {
+ if (_wrapNeeded)
+ {
+ SetNeedsDraw ();
+ }
+ else
+ {
+ // BUGBUG: customized rect aren't supported now because the Redraw isn't using the Intersect method.
+ //SetNeedsDraw (rect);
+ SetNeedsDraw ();
+ }
+ }
+
+ private Attribute? GetSelectedAttribute (int row, int col)
+ {
+ if (!InheritsPreviousAttribute || (Lines == 1 && GetLine (Lines).Count == 0))
+ {
+ return null;
+ }
+
+ List line = GetLine (row);
+ int foundRow = row;
+
+ while (line.Count == 0)
+ {
+ if (foundRow == 0 && line.Count == 0)
+ {
+ return null;
+ }
+
+ foundRow--;
+ line = GetLine (foundRow);
+ }
+
+ int foundCol = foundRow < row ? line.Count - 1 : Math.Min (col, line.Count - 1);
+
+ Cell cell = line [foundCol];
+
+ return cell.Attribute;
+ }
+
+ ///
+ protected override bool OnGettingAttributeForRole (in VisualRole role, ref Attribute currentAttribute)
+ {
+ if (role == VisualRole.Normal)
+ {
+ currentAttribute = GetAttributeForRole (VisualRole.Editable);
+
+ return true;
+ }
+
+ return base.OnGettingAttributeForRole (role, ref currentAttribute);
+ }
+}
diff --git a/Terminal.Gui/Views/TextInput/TextView/TextView.Find.cs b/Terminal.Gui/Views/TextInput/TextView/TextView.Find.cs
new file mode 100644
index 0000000000..35342a3e71
--- /dev/null
+++ b/Terminal.Gui/Views/TextInput/TextView/TextView.Find.cs
@@ -0,0 +1,210 @@
+namespace Terminal.Gui.Views;
+
+/// Find and Replace functionality
+public partial class TextView
+{
+ #region Public Find/Replace Methods
+
+ /// Find the next text based on the match case with the option to replace it.
+ /// The text to find.
+ /// trueIf all the text was forward searched.falseotherwise.
+ /// The match case setting.
+ /// The match whole word setting.
+ /// The text to replace.
+ /// trueIf is replacing.falseotherwise.
+ /// trueIf the text was found.falseotherwise.
+ public bool FindNextText (
+ string textToFind,
+ out bool gaveFullTurn,
+ bool matchCase = false,
+ bool matchWholeWord = false,
+ string? textToReplace = null,
+ bool replace = false
+ )
+ {
+ if (_model.Count == 0)
+ {
+ gaveFullTurn = false;
+
+ return false;
+ }
+
+ SetWrapModel ();
+ ResetContinuousFind ();
+
+ (Point current, bool found) foundPos =
+ _model.FindNextText (textToFind, out gaveFullTurn, matchCase, matchWholeWord);
+
+ return SetFoundText (textToFind, foundPos, textToReplace, replace);
+ }
+
+ /// Find the previous text based on the match case with the option to replace it.
+ /// The text to find.
+ /// trueIf all the text was backward searched.falseotherwise.
+ /// The match case setting.
+ /// The match whole word setting.
+ /// The text to replace.
+ /// trueIf the text was found.falseotherwise.
+ /// trueIf the text was found.falseotherwise.
+ public bool FindPreviousText (
+ string textToFind,
+ out bool gaveFullTurn,
+ bool matchCase = false,
+ bool matchWholeWord = false,
+ string? textToReplace = null,
+ bool replace = false
+ )
+ {
+ if (_model.Count == 0)
+ {
+ gaveFullTurn = false;
+
+ return false;
+ }
+
+ SetWrapModel ();
+ ResetContinuousFind ();
+
+ (Point current, bool found) foundPos =
+ _model.FindPreviousText (textToFind, out gaveFullTurn, matchCase, matchWholeWord);
+
+ return SetFoundText (textToFind, foundPos, textToReplace, replace);
+ }
+
+ /// Reset the flag to stop continuous find.
+ public void FindTextChanged () { _continuousFind = false; }
+
+ /// Replaces all the text based on the match case.
+ /// The text to find.
+ /// The match case setting.
+ /// The match whole word setting.
+ /// The text to replace.
+ /// trueIf the text was found.falseotherwise.
+ public bool ReplaceAllText (
+ string textToFind,
+ bool matchCase = false,
+ bool matchWholeWord = false,
+ string? textToReplace = null
+ )
+ {
+ if (_isReadOnly || _model.Count == 0)
+ {
+ return false;
+ }
+
+ SetWrapModel ();
+ ResetContinuousFind ();
+
+ (Point current, bool found) foundPos =
+ _model.ReplaceAllText (textToFind, matchCase, matchWholeWord, textToReplace);
+
+ return SetFoundText (textToFind, foundPos, textToReplace, false, true);
+ }
+
+ #endregion
+
+ #region Private Find Helper Methods
+
+ private void ResetContinuousFind ()
+ {
+ if (!_continuousFind)
+ {
+ int col = IsSelecting ? _selectionStartColumn : CurrentColumn;
+ int row = IsSelecting ? _selectionStartRow : CurrentRow;
+ _model.ResetContinuousFind (new (col, row));
+ }
+ }
+
+ private void ResetContinuousFindTrack ()
+ {
+ // Handle some state here - whether the last command was a kill
+ // operation and the column tracking (up/down)
+ _lastWasKill = false;
+ _continuousFind = false;
+ }
+
+ private bool SetFoundText (
+ string text,
+ (Point current, bool found) foundPos,
+ string? textToReplace = null,
+ bool replace = false,
+ bool replaceAll = false
+ )
+ {
+ if (foundPos.found)
+ {
+ StartSelecting ();
+ _selectionStartColumn = foundPos.current.X;
+ _selectionStartRow = foundPos.current.Y;
+
+ if (!replaceAll)
+ {
+ CurrentColumn = _selectionStartColumn + text.GetRuneCount ();
+ }
+ else
+ {
+ CurrentColumn = _selectionStartColumn + textToReplace!.GetRuneCount ();
+ }
+
+ CurrentRow = foundPos.current.Y;
+
+ if (!_isReadOnly && replace)
+ {
+ AdjustScrollPosition ();
+ ClearSelectedRegion ();
+ InsertAllText (textToReplace!);
+ StartSelecting ();
+ _selectionStartColumn = CurrentColumn - textToReplace!.GetRuneCount ();
+ }
+ else
+ {
+ UpdateWrapModel ();
+ SetNeedsDraw ();
+ AdjustScrollPosition ();
+ }
+
+ _continuousFind = true;
+
+ return foundPos.found;
+ }
+
+ UpdateWrapModel ();
+ _continuousFind = false;
+
+ return foundPos.found;
+ }
+
+ private IEnumerable<(int col, int row, Cell rune)> ForwardIterator (int col, int row)
+ {
+ if (col < 0 || row < 0)
+ {
+ yield break;
+ }
+
+ if (row >= _model.Count)
+ {
+ yield break;
+ }
+
+ List line = GetCurrentLine ();
+
+ if (col >= line.Count)
+ {
+ yield break;
+ }
+
+ while (row < _model.Count)
+ {
+ for (int c = col; c < line.Count; c++)
+ {
+ yield return (c, row, line [c]);
+ }
+
+ col = 0;
+ row++;
+ line = GetCurrentLine ();
+ }
+ }
+
+ #endregion
+}
diff --git a/Terminal.Gui/Views/TextInput/TextView/TextView.Insert.cs b/Terminal.Gui/Views/TextInput/TextView/TextView.Insert.cs
new file mode 100644
index 0000000000..014902b82d
--- /dev/null
+++ b/Terminal.Gui/Views/TextInput/TextView/TextView.Insert.cs
@@ -0,0 +1,309 @@
+namespace Terminal.Gui.Views;
+
+public partial class TextView
+{
+ ///
+ /// Inserts the given text at the current cursor position exactly as if the user had just
+ /// typed it
+ ///
+ /// Text to add
+ public void InsertText (string toAdd)
+ {
+ foreach (char ch in toAdd)
+ {
+ Key key;
+
+ try
+ {
+ key = new (ch);
+ }
+ catch (Exception)
+ {
+ throw new ArgumentException (
+ $"Cannot insert character '{ch}' because it does not map to a Key"
+ );
+ }
+
+ InsertText (key);
+
+ if (NeedsDraw)
+ {
+ AdjustScrollPosition ();
+ }
+ else
+ {
+ PositionCursor ();
+ }
+ }
+ }
+
+ private void Insert (Cell cell)
+ {
+ List line = GetCurrentLine ();
+
+ if (Used)
+ {
+ line.Insert (Math.Min (CurrentColumn, line.Count), cell);
+ }
+ else
+ {
+ if (CurrentColumn < line.Count)
+ {
+ line.RemoveAt (CurrentColumn);
+ }
+
+ line.Insert (Math.Min (CurrentColumn, line.Count), cell);
+ }
+
+ int prow = CurrentRow - _topRow;
+
+ if (!_wrapNeeded)
+ {
+ // BUGBUG: customized rect aren't supported now because the Redraw isn't using the Intersect method.
+ //SetNeedsDraw (new (0, prow, Math.Max (Viewport.Width, 0), Math.Max (prow + 1, 0)));
+ SetNeedsDraw ();
+ }
+ }
+
+ private void InsertAllText (string text, bool fromClipboard = false)
+ {
+ if (string.IsNullOrEmpty (text))
+ {
+ return;
+ }
+
+ List> lines;
+
+ if (fromClipboard && text == _copiedText)
+ {
+ lines = _copiedCellsList;
+ }
+ else
+ {
+ // Get selected attribute
+ Attribute? attribute = GetSelectedAttribute (CurrentRow, CurrentColumn);
+ lines = Cell.StringToLinesOfCells (text, attribute);
+ }
+
+ if (lines.Count == 0)
+ {
+ return;
+ }
+
+ SetWrapModel ();
+
+ List line = GetCurrentLine ();
+
+ _historyText.Add ([new (line)], CursorPosition);
+
+ // Optimize single line
+ if (lines.Count == 1)
+ {
+ line.InsertRange (CurrentColumn, lines [0]);
+ CurrentColumn += lines [0].Count;
+
+ _historyText.Add (
+ [new (line)],
+ CursorPosition,
+ TextEditingLineStatus.Replaced
+ );
+
+ if (!_wordWrap && CurrentColumn - _leftColumn > Viewport.Width)
+ {
+ _leftColumn = Math.Max (CurrentColumn - Viewport.Width + 1, 0);
+ }
+
+ if (_wordWrap)
+ {
+ SetNeedsDraw ();
+ }
+ else
+ {
+ // BUGBUG: customized rect aren't supported now because the Redraw isn't using the Intersect method.
+ //SetNeedsDraw (new (0, currentRow - topRow, Viewport.Width, Math.Max (currentRow - topRow + 1, 0)));
+ SetNeedsDraw ();
+ }
+
+ UpdateWrapModel ();
+
+ OnContentsChanged ();
+
+ return;
+ }
+
+ List? rest = null;
+ var lastPosition = 0;
+
+ if (_model.Count > 0 && line.Count > 0 && !_copyWithoutSelection)
+ {
+ // Keep a copy of the rest of the line
+ int restCount = line.Count - CurrentColumn;
+ rest = line.GetRange (CurrentColumn, restCount);
+ line.RemoveRange (CurrentColumn, restCount);
+ }
+
+ // First line is inserted at the current location, the rest is appended
+ line.InsertRange (CurrentColumn, lines [0]);
+
+ //model.AddLine (currentRow, lines [0]);
+
+ List> addedLines = [new (line)];
+
+ for (var i = 1; i < lines.Count; i++)
+ {
+ _model.AddLine (CurrentRow + i, lines [i]);
+
+ addedLines.Add ([.. lines [i]]);
+ }
+
+ if (rest is { })
+ {
+ List last = _model.GetLine (CurrentRow + lines.Count - 1);
+ lastPosition = last.Count;
+ last.InsertRange (last.Count, rest);
+
+ addedLines.Last ().InsertRange (addedLines.Last ().Count, rest);
+ }
+
+ _historyText.Add (addedLines, CursorPosition, TextEditingLineStatus.Added);
+
+ // Now adjust column and row positions
+ CurrentRow += lines.Count - 1;
+ CurrentColumn = rest is { } ? lastPosition : lines [^1].Count;
+ AdjustScrollPosition ();
+
+ _historyText.Add (
+ [new (line)],
+ CursorPosition,
+ TextEditingLineStatus.Replaced
+ );
+
+ UpdateWrapModel ();
+ OnContentsChanged ();
+ }
+
+ private bool InsertText (Key a, Attribute? attribute = null)
+ {
+ //So that special keys like tab can be processed
+ if (_isReadOnly)
+ {
+ return true;
+ }
+
+ SetWrapModel ();
+
+ _historyText.Add ([new (GetCurrentLine ())], CursorPosition);
+
+ if (IsSelecting)
+ {
+ ClearSelectedRegion ();
+ }
+
+ if ((uint)a.KeyCode == '\n')
+ {
+ _model.AddLine (CurrentRow + 1, []);
+ CurrentRow++;
+ CurrentColumn = 0;
+ }
+ else if ((uint)a.KeyCode == '\r')
+ {
+ CurrentColumn = 0;
+ }
+ else
+ {
+ if (Used)
+ {
+ Insert (new () { Grapheme = a.AsRune.ToString (), Attribute = attribute });
+ CurrentColumn++;
+
+ if (CurrentColumn >= _leftColumn + Viewport.Width)
+ {
+ _leftColumn++;
+ SetNeedsDraw ();
+ }
+ }
+ else
+ {
+ Insert (new () { Grapheme = a.AsRune.ToString (), Attribute = attribute });
+ CurrentColumn++;
+ }
+ }
+
+ _historyText.Add (
+ [new (GetCurrentLine ())],
+ CursorPosition,
+ TextEditingLineStatus.Replaced
+ );
+
+ UpdateWrapModel ();
+ OnContentsChanged ();
+
+ return true;
+ }
+
+ #region History Event Handlers
+
+ private void HistoryText_ChangeText (object sender, HistoryTextItemEventArgs obj)
+ {
+ SetWrapModel ();
+
+ if (obj is { })
+ {
+ int startLine = obj.CursorPosition.Y;
+
+ if (obj.RemovedOnAdded is { })
+ {
+ int offset;
+
+ if (obj.IsUndoing)
+ {
+ offset = Math.Max (obj.RemovedOnAdded.Lines.Count - obj.Lines.Count, 1);
+ }
+ else
+ {
+ offset = obj.RemovedOnAdded.Lines.Count - 1;
+ }
+
+ for (var i = 0; i < offset; i++)
+ {
+ if (Lines > obj.RemovedOnAdded.CursorPosition.Y)
+ {
+ _model.RemoveLine (obj.RemovedOnAdded.CursorPosition.Y);
+ }
+ else
+ {
+ break;
+ }
+ }
+ }
+
+ for (var i = 0; i < obj.Lines.Count; i++)
+ {
+ if (i == 0 || obj.LineStatus == TextEditingLineStatus.Original || obj.LineStatus == TextEditingLineStatus.Attribute)
+ {
+ _model.ReplaceLine (startLine, obj.Lines [i]);
+ }
+ else if (obj is { IsUndoing: true, LineStatus: TextEditingLineStatus.Removed }
+ or { IsUndoing: false, LineStatus: TextEditingLineStatus.Added })
+ {
+ _model.AddLine (startLine, obj.Lines [i]);
+ }
+ else if (Lines > obj.CursorPosition.Y + 1)
+ {
+ _model.RemoveLine (obj.CursorPosition.Y + 1);
+ }
+
+ startLine++;
+ }
+
+ CursorPosition = obj.FinalCursorPosition;
+ }
+
+ UpdateWrapModel ();
+
+ AdjustScrollPosition ();
+ OnContentsChanged ();
+ }
+
+ #endregion
+}
diff --git a/Terminal.Gui/Views/TextInput/TextView/TextView.Layout.cs b/Terminal.Gui/Views/TextInput/TextView/TextView.Layout.cs
new file mode 100644
index 0000000000..51a1f7e32a
--- /dev/null
+++ b/Terminal.Gui/Views/TextInput/TextView/TextView.Layout.cs
@@ -0,0 +1,162 @@
+namespace Terminal.Gui.Views;
+
+/// Viewport, scrolling, and content area management methods for TextView
+public partial class TextView
+{
+
+ ///
+ /// Configures the ScrollBars to work with the modern View scrolling system.
+ ///
+ private void ConfigureLayout ()
+ {
+ // Vertical ScrollBar: AutoShow enabled by default as per requirements
+ VerticalScrollBar.AutoShow = true;
+
+ // Horizontal ScrollBar: AutoShow tracks WordWrap as per requirements
+ HorizontalScrollBar.AutoShow = !WordWrap;
+ }
+
+ private void TextView_LayoutComplete (object? sender, LayoutEventArgs e)
+ {
+ _topRow = Viewport.Y;
+ _leftColumn = Viewport.X;
+ WrapTextModel ();
+ UpdateContentSize ();
+ AdjustScrollPosition ();
+ }
+
+ ///
+ /// INTERNAL: Adjusts the scroll position and cursor to ensure the cursor is visible in the viewport.
+ /// This method handles both horizontal and vertical scrolling, word wrap considerations, and syncs
+ /// the internal scroll fields with the Viewport property.
+ ///
+ private void AdjustScrollPosition ()
+ {
+ (int width, int height) offB = GetViewportClipping ();
+ List line = GetCurrentLine ();
+ bool need = NeedsDraw || _wrapNeeded || !Used;
+ (int size, int length) tSize = TextModel.DisplaySize (line, -1, -1, false, TabWidth);
+ (int size, int length) dSize = TextModel.DisplaySize (line, _leftColumn, CurrentColumn, true, TabWidth);
+
+ if (!_wordWrap && CurrentColumn < _leftColumn)
+ {
+ _leftColumn = CurrentColumn;
+ need = true;
+ }
+ else if (!_wordWrap
+ && (CurrentColumn - _leftColumn + 1 > Viewport.Width + offB.width || dSize.size + 1 >= Viewport.Width + offB.width))
+ {
+ _leftColumn = TextModel.CalculateLeftColumn (
+ line,
+ _leftColumn,
+ CurrentColumn,
+ Viewport.Width + offB.width,
+ TabWidth
+ );
+ need = true;
+ }
+ else if ((_wordWrap && _leftColumn > 0) || (dSize.size < Viewport.Width + offB.width && tSize.size < Viewport.Width + offB.width))
+ {
+ if (_leftColumn > 0)
+ {
+ _leftColumn = 0;
+ need = true;
+ }
+ }
+
+ if (CurrentRow < _topRow)
+ {
+ _topRow = CurrentRow;
+ need = true;
+ }
+ else if (CurrentRow - _topRow >= Viewport.Height + offB.height)
+ {
+ _topRow = Math.Min (Math.Max (CurrentRow - Viewport.Height + 1, 0), CurrentRow);
+ need = true;
+ }
+ else if (_topRow > 0 && CurrentRow < _topRow)
+ {
+ _topRow = Math.Max (_topRow - 1, 0);
+ need = true;
+ }
+
+ // Sync Viewport with the internal scroll position
+ if (IsInitialized && (_leftColumn != Viewport.X || _topRow != Viewport.Y))
+ {
+ Viewport = new Rectangle (_leftColumn, _topRow, Viewport.Width, Viewport.Height);
+ }
+
+ if (need)
+ {
+ if (_wrapNeeded)
+ {
+ WrapTextModel ();
+ _wrapNeeded = false;
+ }
+
+ SetNeedsDraw ();
+ }
+ else
+ {
+ if (IsInitialized)
+ {
+ PositionCursor ();
+ }
+ }
+
+ OnUnwrappedCursorPosition ();
+ }
+
+ ///
+ /// INTERNAL: Calculates the viewport clipping caused by the view extending beyond the SuperView's boundaries.
+ /// Returns negative width and height offsets when the viewport extends beyond the SuperView, representing
+ /// how much of the viewport is clipped.
+ ///
+ /// A tuple containing the width and height clipping offsets (negative when clipped).
+ private (int width, int height) GetViewportClipping ()
+ {
+ var w = 0;
+ var h = 0;
+
+ if (SuperView?.Viewport.Right - Viewport.Right < 0)
+ {
+ w = SuperView!.Viewport.Right - Viewport.Right - 1;
+ }
+
+ if (SuperView?.Viewport.Bottom - Viewport.Bottom < 0)
+ {
+ h = SuperView!.Viewport.Bottom - Viewport.Bottom - 1;
+ }
+
+ return (w, h);
+ }
+
+ ///
+ /// INTERNAL: Updates the content size based on the text model dimensions.
+ /// When word wrap is enabled, content width equals viewport width.
+ /// Otherwise, calculates the maximum line width from the entire text model.
+ /// Content height is always the number of lines in the model.
+ ///
+ private void UpdateContentSize ()
+ {
+ int contentHeight = Math.Max (_model.Count, 1);
+
+ // For horizontal size: if word wrap is enabled, content width equals viewport width
+ // Otherwise, calculate the maximum line width from the entire text model
+ int contentWidth;
+
+ if (_wordWrap)
+ {
+ // Word wrap: content width follows viewport width
+ contentWidth = Math.Max (Viewport.Width, 1);
+ }
+ else
+ {
+ // No word wrap: calculate max line width
+ // Cache the current value to avoid recalculating on every call
+ contentWidth = Math.Max (_model.GetMaxVisibleLine (0, _model.Count, TabWidth), 1);
+ }
+
+ SetContentSize (new Size (contentWidth, contentHeight));
+ }
+}
diff --git a/Terminal.Gui/Views/TextInput/TextView/TextView.Navigation.cs b/Terminal.Gui/Views/TextInput/TextView/TextView.Navigation.cs
new file mode 100644
index 0000000000..a6a16cb92c
--- /dev/null
+++ b/Terminal.Gui/Views/TextInput/TextView/TextView.Navigation.cs
@@ -0,0 +1,619 @@
+namespace Terminal.Gui.Views;
+
+/// Navigation functionality - cursor movement and scrolling
+public partial class TextView
+{
+ #region Public Navigation Methods
+
+ /// Will scroll the to the last line and position the cursor there.
+ public void MoveEnd ()
+ {
+ CurrentRow = _model.Count - 1;
+ List line = GetCurrentLine ();
+ CurrentColumn = line.Count;
+ TrackColumn ();
+ DoNeededAction ();
+ }
+
+ /// Will scroll the to the first line and position the cursor there.
+ public void MoveHome ()
+ {
+ CurrentRow = 0;
+ _topRow = 0;
+ CurrentColumn = 0;
+ _leftColumn = 0;
+ TrackColumn ();
+ DoNeededAction ();
+ }
+
+ ///
+ /// Will scroll the to display the specified row at the top if is
+ /// true or will scroll the to display the specified column at the left if
+ /// is false.
+ ///
+ ///
+ /// Row that should be displayed at the top or Column that should be displayed at the left, if the value
+ /// is negative it will be reset to zero
+ ///
+ /// If true (default) the is a row, column otherwise.
+ public void ScrollTo (int idx, bool isRow = true)
+ {
+ if (idx < 0)
+ {
+ idx = 0;
+ }
+
+ if (isRow)
+ {
+ _topRow = Math.Max (idx > _model.Count - 1 ? _model.Count - 1 : idx, 0);
+
+ if (IsInitialized && Viewport.Y != _topRow)
+ {
+ Viewport = Viewport with { Y = _topRow };
+ }
+ }
+ else if (!_wordWrap)
+ {
+ int maxlength = _model.GetMaxVisibleLine (_topRow, _topRow + Viewport.Height, TabWidth);
+ _leftColumn = Math.Max (!_wordWrap && idx > maxlength - 1 ? maxlength - 1 : idx, 0);
+
+ if (IsInitialized && Viewport.X != _leftColumn)
+ {
+ Viewport = Viewport with { X = _leftColumn };
+ }
+ }
+
+ SetNeedsDraw ();
+ }
+
+ #endregion
+
+ #region Private Navigation Methods
+
+ private void MoveBottomEnd ()
+ {
+ ResetAllTrack ();
+
+ if (_shiftSelecting && IsSelecting)
+ {
+ StopSelecting ();
+ }
+
+ MoveEnd ();
+ }
+
+ private void MoveBottomEndExtend ()
+ {
+ ResetAllTrack ();
+ StartSelecting ();
+ MoveEnd ();
+ }
+
+ private bool MoveDown ()
+ {
+ if (CurrentRow + 1 < _model.Count)
+ {
+ if (_columnTrack == -1)
+ {
+ _columnTrack = CurrentColumn;
+ }
+
+ CurrentRow++;
+
+ if (CurrentRow >= _topRow + Viewport.Height)
+ {
+ _topRow++;
+ SetNeedsDraw ();
+ }
+
+ TrackColumn ();
+ PositionCursor ();
+ }
+ else if (CurrentRow > Viewport.Height)
+ {
+ AdjustScrollPosition ();
+ }
+ else
+ {
+ return false;
+ }
+
+ DoNeededAction ();
+
+ return true;
+ }
+
+ private void MoveEndOfLine ()
+ {
+ List currentLine = GetCurrentLine ();
+ CurrentColumn = currentLine.Count;
+ DoNeededAction ();
+ }
+
+ private bool MoveLeft ()
+ {
+ if (CurrentColumn > 0)
+ {
+ CurrentColumn--;
+ }
+ else
+ {
+ if (CurrentRow > 0)
+ {
+ CurrentRow--;
+
+ if (CurrentRow < _topRow)
+ {
+ _topRow--;
+ SetNeedsDraw ();
+ }
+
+ List currentLine = GetCurrentLine ();
+ CurrentColumn = Math.Max (currentLine.Count - (ReadOnly ? 1 : 0), 0);
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ DoNeededAction ();
+
+ return true;
+ }
+
+ private void MovePageDown ()
+ {
+ int nPageDnShift = Viewport.Height - 1;
+
+ if (CurrentRow >= 0 && CurrentRow < _model.Count)
+ {
+ if (_columnTrack == -1)
+ {
+ _columnTrack = CurrentColumn;
+ }
+
+ CurrentRow = CurrentRow + nPageDnShift > _model.Count
+ ? _model.Count > 0 ? _model.Count - 1 : 0
+ : CurrentRow + nPageDnShift;
+
+ if (_topRow < CurrentRow - nPageDnShift)
+ {
+ _topRow = CurrentRow >= _model.Count
+ ? CurrentRow - nPageDnShift
+ : _topRow + nPageDnShift;
+ SetNeedsDraw ();
+ }
+
+ TrackColumn ();
+ PositionCursor ();
+ }
+
+ DoNeededAction ();
+ }
+
+ private void MovePageUp ()
+ {
+ int nPageUpShift = Viewport.Height - 1;
+
+ if (CurrentRow > 0)
+ {
+ if (_columnTrack == -1)
+ {
+ _columnTrack = CurrentColumn;
+ }
+
+ CurrentRow = CurrentRow - nPageUpShift < 0 ? 0 : CurrentRow - nPageUpShift;
+
+ if (CurrentRow < _topRow)
+ {
+ _topRow = _topRow - nPageUpShift < 0 ? 0 : _topRow - nPageUpShift;
+ SetNeedsDraw ();
+ }
+
+ TrackColumn ();
+ PositionCursor ();
+ }
+
+ DoNeededAction ();
+ }
+
+ private bool MoveRight ()
+ {
+ List currentLine = GetCurrentLine ();
+
+ if ((ReadOnly ? CurrentColumn + 1 : CurrentColumn) < currentLine.Count)
+ {
+ CurrentColumn++;
+ }
+ else
+ {
+ if (CurrentRow + 1 < _model.Count)
+ {
+ CurrentRow++;
+ CurrentColumn = 0;
+
+ if (CurrentRow >= _topRow + Viewport.Height)
+ {
+ _topRow++;
+ SetNeedsDraw ();
+ }
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ DoNeededAction ();
+
+ return true;
+ }
+
+ private void MoveLeftStart ()
+ {
+ if (_leftColumn > 0)
+ {
+ SetNeedsDraw ();
+ }
+
+ CurrentColumn = 0;
+ _leftColumn = 0;
+ DoNeededAction ();
+ }
+
+ private void MoveTopHome ()
+ {
+ ResetAllTrack ();
+
+ if (_shiftSelecting && IsSelecting)
+ {
+ StopSelecting ();
+ }
+
+ MoveHome ();
+ }
+
+ private void MoveTopHomeExtend ()
+ {
+ ResetColumnTrack ();
+ StartSelecting ();
+ MoveHome ();
+ }
+
+ private bool MoveUp ()
+ {
+ if (CurrentRow > 0)
+ {
+ if (_columnTrack == -1)
+ {
+ _columnTrack = CurrentColumn;
+ }
+
+ CurrentRow--;
+
+ if (CurrentRow < _topRow)
+ {
+ _topRow--;
+ SetNeedsDraw ();
+ }
+
+ TrackColumn ();
+ PositionCursor ();
+ }
+ else
+ {
+ return false;
+ }
+
+ DoNeededAction ();
+
+ return true;
+ }
+
+ private void MoveWordBackward ()
+ {
+ (int col, int row)? newPos = _model.WordBackward (CurrentColumn, CurrentRow, UseSameRuneTypeForWords);
+
+ if (newPos.HasValue)
+ {
+ CurrentColumn = newPos.Value.col;
+ CurrentRow = newPos.Value.row;
+ }
+
+ DoNeededAction ();
+ }
+
+ private void MoveWordForward ()
+ {
+ (int col, int row)? newPos = _model.WordForward (CurrentColumn, CurrentRow, UseSameRuneTypeForWords);
+
+ if (newPos.HasValue)
+ {
+ CurrentColumn = newPos.Value.col;
+ CurrentRow = newPos.Value.row;
+ }
+
+ DoNeededAction ();
+ }
+
+ #endregion
+
+ #region Process Navigation Methods
+
+ private bool ProcessMoveDown ()
+ {
+ ResetContinuousFindTrack ();
+
+ if (_shiftSelecting && IsSelecting)
+ {
+ StopSelecting ();
+ }
+
+ return MoveDown ();
+ }
+
+ private void ProcessMoveDownExtend ()
+ {
+ ResetColumnTrack ();
+ StartSelecting ();
+ MoveDown ();
+ }
+
+ private void ProcessMoveEndOfLine ()
+ {
+ ResetAllTrack ();
+
+ if (_shiftSelecting && IsSelecting)
+ {
+ StopSelecting ();
+ }
+
+ MoveEndOfLine ();
+ }
+
+ private void ProcessMoveRightEndExtend ()
+ {
+ ResetAllTrack ();
+ StartSelecting ();
+ MoveEndOfLine ();
+ }
+
+ private bool ProcessMoveLeft ()
+ {
+ // if the user presses Left (without any control keys) and they are at the start of the text
+ if (CurrentColumn == 0 && CurrentRow == 0)
+ {
+ if (IsSelecting)
+ {
+ StopSelecting ();
+
+ return true;
+ }
+
+ // do not respond (this lets the key press fall through to navigation system - which usually changes focus backward)
+ return false;
+ }
+
+ ResetAllTrack ();
+
+ if (_shiftSelecting && IsSelecting)
+ {
+ StopSelecting ();
+ }
+
+ MoveLeft ();
+
+ return true;
+ }
+
+ private void ProcessMoveLeftExtend ()
+ {
+ ResetAllTrack ();
+ StartSelecting ();
+ MoveLeft ();
+ }
+
+ private bool ProcessMoveRight ()
+ {
+ // if the user presses Right (without any control keys)
+ // determine where the last cursor position in the text is
+ int lastRow = _model.Count - 1;
+ int lastCol = _model.GetLine (lastRow).Count;
+
+ // if they are at the very end of all the text do not respond (this lets the key press fall through to navigation system - which usually changes focus forward)
+ if (CurrentColumn == lastCol && CurrentRow == lastRow)
+ {
+ // Unless they have text selected
+ if (IsSelecting)
+ {
+ // In which case clear
+ StopSelecting ();
+
+ return true;
+ }
+
+ return false;
+ }
+
+ ResetAllTrack ();
+
+ if (_shiftSelecting && IsSelecting)
+ {
+ StopSelecting ();
+ }
+
+ MoveRight ();
+
+ return true;
+ }
+
+ private void ProcessMoveRightExtend ()
+ {
+ ResetAllTrack ();
+ StartSelecting ();
+ MoveRight ();
+ }
+
+ private void ProcessMoveLeftStart ()
+ {
+ ResetAllTrack ();
+
+ if (_shiftSelecting && IsSelecting)
+ {
+ StopSelecting ();
+ }
+
+ MoveLeftStart ();
+ }
+
+ private void ProcessMoveLeftStartExtend ()
+ {
+ ResetAllTrack ();
+ StartSelecting ();
+ MoveLeftStart ();
+ }
+
+ private bool ProcessMoveUp ()
+ {
+ ResetContinuousFindTrack ();
+
+ if (_shiftSelecting && IsSelecting)
+ {
+ StopSelecting ();
+ }
+
+ return MoveUp ();
+ }
+
+ private void ProcessMoveUpExtend ()
+ {
+ ResetColumnTrack ();
+ StartSelecting ();
+ MoveUp ();
+ }
+
+ private void ProcessMoveWordBackward ()
+ {
+ ResetAllTrack ();
+
+ if (_shiftSelecting && IsSelecting)
+ {
+ StopSelecting ();
+ }
+
+ MoveWordBackward ();
+ }
+
+ private void ProcessMoveWordBackwardExtend ()
+ {
+ ResetAllTrack ();
+ StartSelecting ();
+ MoveWordBackward ();
+ }
+
+ private void ProcessMoveWordForward ()
+ {
+ ResetAllTrack ();
+
+ if (_shiftSelecting && IsSelecting)
+ {
+ StopSelecting ();
+ }
+
+ MoveWordForward ();
+ }
+
+ private void ProcessMoveWordForwardExtend ()
+ {
+ ResetAllTrack ();
+ StartSelecting ();
+ MoveWordForward ();
+ }
+
+ private void ProcessPageDown ()
+ {
+ ResetColumnTrack ();
+
+ if (_shiftSelecting && IsSelecting)
+ {
+ StopSelecting ();
+ }
+
+ MovePageDown ();
+ }
+
+ private void ProcessPageDownExtend ()
+ {
+ ResetColumnTrack ();
+ StartSelecting ();
+ MovePageDown ();
+ }
+
+ private void ProcessPageUp ()
+ {
+ ResetColumnTrack ();
+
+ if (_shiftSelecting && IsSelecting)
+ {
+ StopSelecting ();
+ }
+
+ MovePageUp ();
+ }
+
+ private void ProcessPageUpExtend ()
+ {
+ ResetColumnTrack ();
+ StartSelecting ();
+ MovePageUp ();
+ }
+
+ #endregion
+
+ #region Column Tracking
+
+ // Tries to snap the cursor to the tracking column
+ private void TrackColumn ()
+ {
+ // Now track the column
+ List line = GetCurrentLine ();
+
+ if (line.Count < _columnTrack)
+ {
+ CurrentColumn = line.Count;
+ }
+ else if (_columnTrack != -1)
+ {
+ CurrentColumn = _columnTrack;
+ }
+ else if (CurrentColumn > line.Count)
+ {
+ CurrentColumn = line.Count;
+ }
+
+ AdjustScrollPosition ();
+ }
+
+ #endregion
+
+
+ private void ResetAllTrack ()
+ {
+ // Handle some state here - whether the last command was a kill
+ // operation and the column tracking (up/down)
+ _lastWasKill = false;
+ _columnTrack = -1;
+ _continuousFind = false;
+ }
+
+ ///
+ /// INTERNAL: Resets the cursor position and scroll offsets to the beginning of the document (0,0)
+ /// and stops any active text selection.
+ ///
+ private void ResetPosition ()
+ {
+ _topRow = _leftColumn = CurrentRow = CurrentColumn = 0;
+ StopSelecting ();
+ }
+}
diff --git a/Terminal.Gui/Views/TextInput/TextView/TextView.Selection.cs b/Terminal.Gui/Views/TextInput/TextView/TextView.Selection.cs
new file mode 100644
index 0000000000..21ca9ae1b6
--- /dev/null
+++ b/Terminal.Gui/Views/TextInput/TextView/TextView.Selection.cs
@@ -0,0 +1,407 @@
+namespace Terminal.Gui.Views;
+
+public partial class TextView
+{
+
+ /// Get or sets whether the user is currently selecting text.
+ public bool IsSelecting { get; set; }
+
+ ///
+ /// 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 false meaning that the spaces at right are included in the selection.
+ ///
+ public bool SelectWordOnlyOnDoubleClick { get; set; }
+
+ /// Start row position of the selected text.
+ public int SelectionStartRow
+ {
+ get => _selectionStartRow;
+ set
+ {
+ _selectionStartRow = value < 0 ? 0 :
+ value > _model.Count - 1 ? Math.Max (_model.Count - 1, 0) : value;
+ IsSelecting = true;
+ SetNeedsDraw ();
+ AdjustScrollPosition ();
+ }
+ }
+
+ /// Start column position of the selected text.
+ public int SelectionStartColumn
+ {
+ get => _selectionStartColumn;
+ set
+ {
+ List line = _model.GetLine (_selectionStartRow);
+
+ _selectionStartColumn = value < 0 ? 0 :
+ value > line.Count ? line.Count : value;
+ IsSelecting = true;
+ SetNeedsDraw ();
+ AdjustScrollPosition ();
+ }
+ }
+
+ private void StartSelecting ()
+ {
+ if (_shiftSelecting && IsSelecting)
+ {
+ return;
+ }
+
+ _shiftSelecting = true;
+ IsSelecting = true;
+ _selectionStartColumn = CurrentColumn;
+ _selectionStartRow = CurrentRow;
+ }
+
+ private void StopSelecting ()
+ {
+ if (IsSelecting)
+ {
+ SetNeedsDraw ();
+ }
+
+ _shiftSelecting = false;
+ IsSelecting = false;
+ _isButtonShift = false;
+ }
+
+
+ /// Length of the selected text.
+ public int SelectedLength => GetSelectedLength ();
+
+ ///
+ /// Gets the selected text as
+ ///
+ /// List{List{Cell}}
+ ///
+ ///
+ public List> SelectedCellsList
+ {
+ get
+ {
+ GetRegion (out List> selectedCellsList);
+
+ return selectedCellsList;
+ }
+ }
+
+ /// The selected text.
+ public string SelectedText
+ {
+ get
+ {
+ if (!IsSelecting || (_model.Count == 1 && _model.GetLine (0).Count == 0))
+ {
+ return string.Empty;
+ }
+
+ return GetSelectedRegion ();
+ }
+ }
+
+
+ // Returns an encoded region start..end (top 32 bits are the row, low32 the column)
+ private void GetEncodedRegionBounds (
+ out long start,
+ out long end,
+ int? startRow = null,
+ int? startCol = null,
+ int? cRow = null,
+ int? cCol = null
+ )
+ {
+ long selection;
+ long point;
+
+ if (startRow is null || startCol is null || cRow is null || cCol is null)
+ {
+ selection = ((long)(uint)_selectionStartRow << 32) | (uint)_selectionStartColumn;
+ point = ((long)(uint)CurrentRow << 32) | (uint)CurrentColumn;
+ }
+ else
+ {
+ selection = ((long)(uint)startRow << 32) | (uint)startCol;
+ point = ((long)(uint)cRow << 32) | (uint)cCol;
+ }
+
+ if (selection > point)
+ {
+ start = point;
+ end = selection;
+ }
+ else
+ {
+ start = selection;
+ end = point;
+ }
+ }
+
+ //
+ // Returns a string with the text in the selected
+ // region.
+ //
+ internal string GetRegion (
+ out List> cellsList,
+ int? sRow = null,
+ int? sCol = null,
+ int? cRow = null,
+ int? cCol = null,
+ TextModel? model = null
+ )
+ {
+ GetEncodedRegionBounds (out long start, out long end, sRow, sCol, cRow, cCol);
+
+ cellsList = [];
+
+ if (start == end)
+ {
+ return string.Empty;
+ }
+
+ var startRow = (int)(start >> 32);
+ var maxRow = (int)(end >> 32);
+ var startCol = (int)(start & 0xffffffff);
+ var endCol = (int)(end & 0xffffffff);
+ List line = model is null ? _model.GetLine (startRow) : model.GetLine (startRow);
+ List cells;
+
+ if (startRow == maxRow)
+ {
+ cells = line.GetRange (startCol, endCol - startCol);
+ cellsList.Add (cells);
+
+ return StringFromCells (cells);
+ }
+
+ cells = line.GetRange (startCol, line.Count - startCol);
+ cellsList.Add (cells);
+ string res = StringFromCells (cells);
+
+ for (int row = startRow + 1; row < maxRow; row++)
+ {
+ cellsList.AddRange ([]);
+ cells = model == null ? _model.GetLine (row) : model.GetLine (row);
+ cellsList.Add (cells);
+
+ res = res
+ + Environment.NewLine
+ + StringFromCells (cells);
+ }
+
+ line = model is null ? _model.GetLine (maxRow) : model.GetLine (maxRow);
+ cellsList.AddRange ([]);
+ cells = line.GetRange (0, endCol);
+ cellsList.Add (cells);
+ res = res + Environment.NewLine + StringFromCells (cells);
+
+ return res;
+ }
+
+ private int GetSelectedLength () { return SelectedText.Length; }
+
+ private string GetSelectedRegion ()
+ {
+ int cRow = CurrentRow;
+ int cCol = CurrentColumn;
+ int startRow = _selectionStartRow;
+ int startCol = _selectionStartColumn;
+ TextModel model = _model;
+
+ if (_wordWrap)
+ {
+ cRow = _wrapManager!.GetModelLineFromWrappedLines (CurrentRow);
+ cCol = _wrapManager.GetModelColFromWrappedLines (CurrentRow, CurrentColumn);
+ startRow = _wrapManager.GetModelLineFromWrappedLines (_selectionStartRow);
+ startCol = _wrapManager.GetModelColFromWrappedLines (_selectionStartRow, _selectionStartColumn);
+ model = _wrapManager.Model;
+ }
+
+ OnUnwrappedCursorPosition (cRow, cCol);
+
+ return GetRegion (out _, startRow, startCol, cRow, cCol, model);
+ }
+
+
+ private string StringFromCells (List cells)
+ {
+ ArgumentNullException.ThrowIfNull (cells);
+
+ var size = 0;
+ foreach (Cell cell in cells)
+ {
+ string t = cell.Grapheme;
+ size += Encoding.Unicode.GetByteCount (t);
+ }
+
+ byte [] encoded = new byte [size];
+ var offset = 0;
+ foreach (Cell cell in cells)
+ {
+ string t = cell.Grapheme;
+ int bytesWritten = Encoding.Unicode.GetBytes (t, 0, t.Length, encoded, offset);
+ offset += bytesWritten;
+ }
+
+ // decode using the same encoding and the bytes actually written
+ return Encoding.Unicode.GetString (encoded, 0, offset);
+ }
+
+ ///
+ public bool EnableForDesign ()
+ {
+ Text = """
+ TextView provides a fully featured multi-line text editor.
+ It supports word wrap and history for undo.
+ """;
+
+ return true;
+ }
+
+
+ ///
+ protected override void Dispose (bool disposing)
+ {
+ if (disposing && ContextMenu is { })
+ {
+ ContextMenu.Visible = false;
+ ContextMenu.Dispose ();
+ ContextMenu = null;
+ }
+
+ base.Dispose (disposing);
+ }
+
+ private void ClearRegion ()
+ {
+ SetWrapModel ();
+
+ long start, end;
+ long currentEncoded = ((long)(uint)CurrentRow << 32) | (uint)CurrentColumn;
+ GetEncodedRegionBounds (out start, out end);
+ var startRow = (int)(start >> 32);
+ var maxrow = (int)(end >> 32);
+ var startCol = (int)(start & 0xffffffff);
+ var endCol = (int)(end & 0xffffffff);
+ List line = _model.GetLine (startRow);
+
+ _historyText.Add (new () { new (line) }, new (startCol, startRow));
+
+ List> removedLines = new ();
+
+ if (startRow == maxrow)
+ {
+ removedLines.Add (new (line));
+
+ line.RemoveRange (startCol, endCol - startCol);
+ CurrentColumn = startCol;
+
+ if (_wordWrap)
+ {
+ SetNeedsDraw ();
+ }
+ else
+ {
+ //QUESTION: Is the below comment still relevant?
+ // BUGBUG: customized rect aren't supported now because the Redraw isn't using the Intersect method.
+ //SetNeedsDraw (new (0, startRow - topRow, Viewport.Width, startRow - topRow + 1));
+ SetNeedsDraw ();
+ }
+
+ _historyText.Add (
+ new (removedLines),
+ CursorPosition,
+ TextEditingLineStatus.Removed
+ );
+
+ UpdateWrapModel ();
+
+ return;
+ }
+
+ removedLines.Add (new (line));
+
+ line.RemoveRange (startCol, line.Count - startCol);
+ List line2 = _model.GetLine (maxrow);
+ line.AddRange (line2.Skip (endCol));
+
+ for (int row = startRow + 1; row <= maxrow; row++)
+ {
+ removedLines.Add (new (_model.GetLine (startRow + 1)));
+
+ _model.RemoveLine (startRow + 1);
+ }
+
+ if (currentEncoded == end)
+ {
+ CurrentRow -= maxrow - startRow;
+ }
+
+ CurrentColumn = startCol;
+
+ _historyText.Add (
+ new (removedLines),
+ CursorPosition,
+ TextEditingLineStatus.Removed
+ );
+
+ UpdateWrapModel ();
+
+ SetNeedsDraw ();
+ }
+
+ private void ClearSelectedRegion ()
+ {
+ SetWrapModel ();
+
+ if (!_isReadOnly)
+ {
+ ClearRegion ();
+ }
+
+ UpdateWrapModel ();
+ IsSelecting = false;
+ DoNeededAction ();
+ }
+
+ /// Select all text.
+ public void SelectAll ()
+ {
+ if (_model.Count == 0)
+ {
+ return;
+ }
+
+ StartSelecting ();
+ _selectionStartColumn = 0;
+ _selectionStartRow = 0;
+ CurrentColumn = _model.GetLine (_model.Count - 1).Count;
+ CurrentRow = _model.Count - 1;
+ SetNeedsDraw ();
+ }
+
+ private void ProcessSelectAll ()
+ {
+ ResetColumnTrack ();
+ SelectAll ();
+ }
+
+ private bool PointInSelection (int col, int row)
+ {
+ long start, end;
+ GetEncodedRegionBounds (out start, out end);
+ long q = ((long)(uint)row << 32) | (uint)col;
+
+ return q >= start && q <= end - 1;
+ }
+
+ private void ToggleSelecting ()
+ {
+ ResetColumnTrack ();
+ IsSelecting = !IsSelecting;
+ _selectionStartColumn = CurrentColumn;
+ _selectionStartRow = CurrentRow;
+ }
+}
diff --git a/Terminal.Gui/Views/TextInput/TextView/TextView.WordWrap.cs b/Terminal.Gui/Views/TextInput/TextView/TextView.WordWrap.cs
new file mode 100644
index 0000000000..99b26a8e4e
--- /dev/null
+++ b/Terminal.Gui/Views/TextInput/TextView/TextView.WordWrap.cs
@@ -0,0 +1,125 @@
+using System.Runtime.CompilerServices;
+
+namespace Terminal.Gui.Views;
+
+/// Word wrap functionality
+public partial class TextView
+{
+ /// Invoke the event with the unwrapped .
+ public virtual void OnUnwrappedCursorPosition (int? cRow = null, int? cCol = null)
+ {
+ int? row = cRow ?? CurrentRow;
+ int? col = cCol ?? CurrentColumn;
+
+ if (cRow is null && cCol is null && _wordWrap)
+ {
+ row = _wrapManager!.GetModelLineFromWrappedLines (CurrentRow);
+ col = _wrapManager.GetModelColFromWrappedLines (CurrentRow, CurrentColumn);
+ }
+
+ UnwrappedCursorPosition?.Invoke (this, new (col.Value, row.Value));
+ }
+
+ /// Invoked with the unwrapped .
+ public event EventHandler? UnwrappedCursorPosition;
+
+ private (int Row, int Col) GetUnwrappedPosition (int line, int col)
+ {
+ if (WordWrap)
+ {
+ return new ValueTuple (
+ _wrapManager!.GetModelLineFromWrappedLines (line),
+ _wrapManager.GetModelColFromWrappedLines (line, col)
+ );
+ }
+
+ return new ValueTuple (line, col);
+ }
+
+ /// Restore from original model.
+ private void SetWrapModel ([CallerMemberName] string? caller = null)
+ {
+ if (_currentCaller is { })
+ {
+ return;
+ }
+
+ if (_wordWrap)
+ {
+ _currentCaller = caller;
+
+ CurrentColumn = _wrapManager!.GetModelColFromWrappedLines (CurrentRow, CurrentColumn);
+ CurrentRow = _wrapManager.GetModelLineFromWrappedLines (CurrentRow);
+
+ _selectionStartColumn =
+ _wrapManager.GetModelColFromWrappedLines (_selectionStartRow, _selectionStartColumn);
+ _selectionStartRow = _wrapManager.GetModelLineFromWrappedLines (_selectionStartRow);
+ _model = _wrapManager.Model;
+ }
+ }
+
+ /// Update the original model.
+ private void UpdateWrapModel ([CallerMemberName] string? caller = null)
+ {
+ if (_currentCaller is { } && _currentCaller != caller)
+ {
+ return;
+ }
+
+ if (_wordWrap)
+ {
+ _currentCaller = null;
+
+ _wrapManager!.UpdateModel (
+ _model,
+ out int nRow,
+ out int nCol,
+ out int nStartRow,
+ out int nStartCol,
+ CurrentRow,
+ CurrentColumn,
+ _selectionStartRow,
+ _selectionStartColumn,
+ true
+ );
+ CurrentRow = nRow;
+ CurrentColumn = nCol;
+ _selectionStartRow = nStartRow;
+ _selectionStartColumn = nStartCol;
+ _wrapNeeded = true;
+
+ SetNeedsDraw ();
+ }
+
+ if (_currentCaller is { })
+ {
+ throw new InvalidOperationException (
+ $"WordWrap settings was changed after the {_currentCaller} call."
+ );
+ }
+ }
+
+ private void WrapTextModel ()
+ {
+ if (_wordWrap && _wrapManager is { })
+ {
+ _model = _wrapManager.WrapModel (
+ Math.Max (Viewport.Width - (ReadOnly ? 0 : 1), 0), // For the cursor on the last column of a line
+ out int nRow,
+ out int nCol,
+ out int nStartRow,
+ out int nStartCol,
+ CurrentRow,
+ CurrentColumn,
+ _selectionStartRow,
+ _selectionStartColumn,
+ _tabWidth
+ );
+ CurrentRow = nRow;
+ CurrentColumn = nCol;
+ _selectionStartRow = nStartRow;
+ _selectionStartColumn = nStartCol;
+ SetNeedsDraw ();
+ }
+ }
+}
diff --git a/Terminal.Gui/Views/TextInput/TextView/TextView.cs b/Terminal.Gui/Views/TextInput/TextView/TextView.cs
new file mode 100644
index 0000000000..19cb32f02e
--- /dev/null
+++ b/Terminal.Gui/Views/TextInput/TextView/TextView.cs
@@ -0,0 +1,1328 @@
+using System.Globalization;
+using System.Runtime.CompilerServices;
+
+namespace Terminal.Gui.Views;
+
+/// Fully featured multi-line text editor
+///
+///
+///
+/// Shortcut Action performed
+///
+/// -
+/// Left cursor, Control-b Moves the editing point left.
+///
+/// -
+/// Right cursor, Control-f Moves the editing point right.
+///
+/// -
+/// Alt-b Moves one word back.
+///
+/// -
+/// Alt-f Moves one word forward.
+///
+/// -
+/// Up cursor, Control-p Moves the editing point one line up.
+///
+/// -
+/// Down cursor, Control-n Moves the editing point one line down
+///
+/// -
+/// Home key, Control-a Moves the cursor to the beginning of the line.
+///
+/// -
+/// End key, Control-e Moves the cursor to the end of the line.
+///
+/// -
+/// Control-Home Scrolls to the first line and moves the cursor there.
+///
+/// -
+/// Control-End Scrolls to the last line and moves the cursor there.
+///
+/// -
+/// Delete, Control-d Deletes the character in front of the cursor.
+///
+/// -
+/// Backspace Deletes the character behind the cursor.
+///
+/// -
+/// Control-k
+///
+/// Deletes the text until the end of the line and replaces the kill buffer with the deleted text.
+/// You can paste this text in a different place by using Control-y.
+///
+///
+/// -
+/// Control-y
+/// Pastes the content of the kill ring into the current position.
+///
+/// -
+/// Alt-d
+///
+/// Deletes the word above the cursor and adds it to the kill ring. You can paste the contents of
+/// the kill ring with Control-y.
+///
+///
+/// -
+/// Control-q
+///
+/// Quotes the next input character, to prevent the normal processing of key handling to take
+/// place.
+///
+///
+///
+///
+public partial class TextView : View, IDesignable
+{
+ // BUGBUG: AllowsReturn is mis-named. It should be EnterKeyAccepts.
+ ///
+ /// Gets or sets whether pressing ENTER in a creates a new line of text
+ /// in the view or invokes the event.
+ ///
+ ///
+ ///
+ /// Setting this property alters .
+ /// If is set to , then is also set to
+ /// `true` and
+ /// vice-versa.
+ ///
+ ///
+ /// If is set to , then gets set to
+ /// .
+ ///
+ ///
+ public bool AllowsReturn
+ {
+ get => _allowsReturn;
+ set
+ {
+ _allowsReturn = value;
+
+ if (_allowsReturn && !_multiline)
+ {
+ // BUGBUG: Setting properties should not have side-effects like this. Multiline and AllowsReturn should be independent.
+ Multiline = true;
+ }
+
+ if (!_allowsReturn && _multiline)
+ {
+ Multiline = false;
+
+ // BUGBUG: Setting properties should not have side-effects like this. Multiline and AllowsTab should be independent.
+ AllowsTab = false;
+ }
+
+ SetNeedsDraw ();
+ }
+ }
+
+ ///
+ /// Gets or sets whether the inserts a tab character into the text or ignores tab input. If
+ /// set to `false` and the user presses the tab key (or shift-tab) the focus will move to the next view (or previous
+ /// with shift-tab). The default is `true`; if the user presses the tab key, a tab character will be inserted into the
+ /// text.
+ ///
+ public bool AllowsTab
+ {
+ get => _allowsTab;
+ set
+ {
+ _allowsTab = value;
+
+ if (_allowsTab && _tabWidth == 0)
+ {
+ _tabWidth = 4;
+ }
+
+ if (_allowsTab && !_multiline)
+ {
+ Multiline = true;
+ }
+
+ if (!_allowsTab && _tabWidth > 0)
+ {
+ _tabWidth = 0;
+ }
+
+ SetNeedsDraw ();
+ }
+ }
+
+ ///
+ /// Provides autocomplete context menu based on suggestions at the current cursor position. Configure
+ /// to enable this feature
+ ///
+ public IAutocomplete Autocomplete { get; protected set; } = new TextViewAutocomplete ();
+
+ /// Get the Context Menu.
+ public PopoverMenu? ContextMenu { get; private set; }
+
+ /// Gets the cursor column.
+ /// The cursor column.
+ public int CurrentColumn { get; private set; }
+
+ /// Gets the current cursor row.
+ public int CurrentRow { get; private set; }
+
+ /// Sets or gets the current cursor position.
+ public Point CursorPosition
+ {
+ get => new (CurrentColumn, CurrentRow);
+ set
+ {
+ List line = _model.GetLine (Math.Max (Math.Min (value.Y, _model.Count - 1), 0));
+
+ CurrentColumn = value.X < 0 ? 0 :
+ value.X > line.Count ? line.Count : value.X;
+
+ CurrentRow = value.Y < 0 ? 0 :
+ value.Y > _model.Count - 1 ? Math.Max (_model.Count - 1, 0) : value.Y;
+ SetNeedsDraw ();
+ AdjustScrollPosition ();
+ }
+ }
+
+ ///
+ /// Indicates whatever the text has history changes or not. if the text has history changes
+ /// otherwise.
+ ///
+ public bool HasHistoryChanges => _historyText.HasHistoryChanges;
+
+ ///
+ /// If and the current is null will inherit from the
+ /// previous, otherwise if (default) do nothing. If the text is load with
+ /// this property is automatically sets to .
+ ///
+ public bool InheritsPreviousAttribute { get; set; }
+
+ ///
+ /// Indicates whatever the text was changed or not. if the text was changed
+ /// otherwise.
+ ///
+ public bool IsDirty
+ {
+ get => _historyText.IsDirty (_model.GetAllLines ());
+ set => _historyText.Clear (_model.GetAllLines ());
+ }
+
+ /// Gets or sets the left column.
+ public int LeftColumn
+ {
+ get => _leftColumn;
+ set
+ {
+ if (value > 0 && _wordWrap)
+ {
+ return;
+ }
+
+ int clampedValue = Math.Max (Math.Min (value, Maxlength - 1), 0);
+ _leftColumn = clampedValue;
+
+ if (IsInitialized && Viewport.X != _leftColumn)
+ {
+ Viewport = Viewport with { X = _leftColumn };
+ }
+ }
+ }
+
+ /// Gets the number of lines.
+ public int Lines => _model.Count;
+
+ /// Gets the maximum visible length line.
+ public int Maxlength => _model.GetMaxVisibleLine (_topRow, _topRow + Viewport.Height, TabWidth);
+
+ /// Gets or sets a value indicating whether this is a multiline text view.
+ public bool Multiline
+ {
+ get => _multiline;
+ set
+ {
+ _multiline = value;
+
+ if (_multiline && !_allowsTab)
+ {
+ AllowsTab = true;
+ }
+
+ if (_multiline && !_allowsReturn)
+ {
+ AllowsReturn = true;
+ }
+
+ if (!_multiline)
+ {
+ AllowsReturn = false;
+ AllowsTab = false;
+ WordWrap = false;
+ CurrentColumn = 0;
+ CurrentRow = 0;
+ _savedHeight = Height;
+
+ Height = Dim.Auto (DimAutoStyle.Text, 1);
+
+ if (!IsInitialized)
+ {
+ _model.LoadString (Text);
+ }
+
+ SetNeedsDraw ();
+ }
+ else if (_multiline && _savedHeight is { })
+ {
+ Height = _savedHeight;
+ SetNeedsDraw ();
+ }
+
+ KeyBindings.Remove (Key.Enter);
+ KeyBindings.Add (Key.Enter, Multiline ? Command.NewLine : Command.Accept);
+ }
+ }
+
+ /// Gets or sets whether the is in read-only mode or not
+ /// Boolean value(Default false)
+ public bool ReadOnly
+ {
+ get => _isReadOnly;
+ set
+ {
+ if (value != _isReadOnly)
+ {
+ _isReadOnly = value;
+
+ SetNeedsDraw ();
+ WrapTextModel ();
+ AdjustScrollPosition ();
+ }
+ }
+ }
+
+ /// Gets or sets a value indicating the number of whitespace when pressing the TAB key.
+ public int TabWidth
+ {
+ get => _tabWidth;
+ set
+ {
+ _tabWidth = Math.Max (value, 0);
+
+ if (_tabWidth > 0 && !AllowsTab)
+ {
+ AllowsTab = true;
+ }
+
+ SetNeedsDraw ();
+ }
+ }
+
+ /// Sets or gets the text in the .
+ ///
+ /// The event is fired whenever this property is set. Note, however, that Text is not
+ /// set by as the user types.
+ ///
+ public override string Text
+ {
+ get
+ {
+ if (_wordWrap)
+ {
+ return _wrapManager!.Model.ToString ();
+ }
+
+ return _model.ToString ();
+ }
+ set
+ {
+ ResetPosition ();
+ _model.LoadString (value);
+
+ if (_wordWrap)
+ {
+ _wrapManager = new (_model);
+ _model = _wrapManager.WrapModel (Viewport.Width, out _, out _, out _, out _);
+ }
+
+ OnTextChanged ();
+ SetNeedsDraw ();
+
+ _historyText.Clear (_model.GetAllLines ());
+ }
+ }
+
+ /// Gets or sets the top row.
+ public int TopRow
+ {
+ get => _topRow;
+ set
+ {
+ int clampedValue = Math.Max (Math.Min (value, Lines - 1), 0);
+ _topRow = clampedValue;
+
+ if (IsInitialized && Viewport.Y != _topRow)
+ {
+ Viewport = Viewport with { Y = _topRow };
+ }
+ }
+ }
+
+ ///
+ /// Tracks whether the text view should be considered "used", that is, that the user has moved in the entry, so
+ /// new input should be appended at the cursor position, rather than clearing the entry
+ ///
+ public bool Used { get; set; }
+
+ /// Allows word wrap the to fit the available container width.
+ public bool WordWrap
+ {
+ get => _wordWrap;
+ set
+ {
+ if (value == _wordWrap)
+ {
+ return;
+ }
+
+ if (value && !_multiline)
+ {
+ return;
+ }
+
+ _wordWrap = value;
+ ResetPosition ();
+
+ if (_wordWrap)
+ {
+ _wrapManager = new (_model);
+ WrapTextModel ();
+ }
+ else if (!_wordWrap && _wrapManager is { })
+ {
+ _model = _wrapManager.Model;
+ }
+
+ // Update horizontal scrollbar AutoShow based on WordWrap
+ if (IsInitialized)
+ {
+ HorizontalScrollBar.AutoShow = !_wordWrap;
+ UpdateContentSize ();
+ }
+
+ SetNeedsDraw ();
+ }
+ }
+
+ ///
+ /// Gets or sets whether the word forward and word backward navigation should use the same or equivalent rune type.
+ /// Default is false meaning using equivalent rune type.
+ ///
+ public bool UseSameRuneTypeForWords { get; set; }
+
+
+
+ /// Allows clearing the items updating the original text.
+ public void ClearHistoryChanges () { _historyText?.Clear (_model.GetAllLines ()); }
+
+ /// Closes the contents of the stream into the .
+ /// true, if stream was closed, false otherwise.
+ public bool CloseFile ()
+ {
+ SetWrapModel ();
+ bool res = _model.CloseFile ();
+ ResetPosition ();
+ SetNeedsDraw ();
+ UpdateWrapModel ();
+
+ return res;
+ }
+
+ /// Raised when the contents of the are changed.
+ ///
+ /// Unlike the event, this event is raised whenever the user types or otherwise changes
+ /// the contents of the .
+ ///
+ public event EventHandler? ContentsChanged;
+
+ ///
+ /// Open a dialog to set the foreground and background colors.
+ ///
+ public void PromptForColors ()
+ {
+ if (!ColorPicker.Prompt (
+ "Colors",
+ GetSelectedCellAttribute (),
+ out Attribute newAttribute
+ ))
+ {
+ return;
+ }
+
+ var attribute = new Attribute (
+ newAttribute.Foreground,
+ newAttribute.Background,
+ newAttribute.Style
+ );
+
+ ApplyCellsAttribute (attribute);
+ }
+
+ /// Gets all lines of characters.
+ ///
+ public List> GetAllLines () { return _model.GetAllLines (); }
+
+ ///
+ /// Returns the characters on the current line (where the cursor is positioned). Use
+ /// to determine the position of the cursor within that line
+ ///
+ ///
+ public List GetCurrentLine () { return _model.GetLine (CurrentRow); }
+
+ /// Returns the characters on the .
+ /// The intended line.
+ ///
+ public List GetLine (int line) { return _model.GetLine (line); }
+
+ /// Loads the contents of the file into the .
+ /// true, if file was loaded, false otherwise.
+ /// Path to the file to load.
+ public bool Load (string path)
+ {
+ SetWrapModel ();
+ bool res;
+
+ try
+ {
+ SetWrapModel ();
+ res = _model.LoadFile (path);
+ _historyText.Clear (_model.GetAllLines ());
+ ResetPosition ();
+ }
+ finally
+ {
+ UpdateWrapModel ();
+ SetNeedsDraw ();
+ AdjustScrollPosition ();
+ }
+
+ UpdateWrapModel ();
+
+ return res;
+ }
+
+ /// Loads the contents of the stream into the .
+ /// true, if stream was loaded, false otherwise.
+ /// Stream to load the contents from.
+ public void Load (Stream stream)
+ {
+ SetWrapModel ();
+ _model.LoadStream (stream);
+ _historyText.Clear (_model.GetAllLines ());
+ ResetPosition ();
+ SetNeedsDraw ();
+ UpdateWrapModel ();
+ }
+
+ /// Loads the contents of the list into the .
+ /// Text cells list to load the contents from.
+ public void Load (List cells)
+ {
+ SetWrapModel ();
+ _model.LoadCells (cells, GetAttributeForRole (VisualRole.Focus));
+ _historyText.Clear (_model.GetAllLines ());
+ ResetPosition ();
+ SetNeedsDraw ();
+ UpdateWrapModel ();
+ InheritsPreviousAttribute = true;
+ }
+
+ /// Loads the contents of the list of list into the .
+ /// List of rune cells list to load the contents from.
+ public void Load (List> cellsList)
+ {
+ SetWrapModel ();
+ InheritsPreviousAttribute = true;
+ _model.LoadListCells (cellsList, GetAttributeForRole (VisualRole.Focus));
+ _historyText.Clear (_model.GetAllLines ());
+ ResetPosition ();
+ SetNeedsDraw ();
+ UpdateWrapModel ();
+ }
+
+ ///
+ protected override bool OnMouseEvent (MouseEventArgs ev)
+ {
+ if (ev is { IsSingleDoubleOrTripleClicked: false, IsPressed: false, IsReleased: false, IsWheel: false }
+ && !ev.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition)
+ && !ev.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ButtonShift)
+ && !ev.Flags.HasFlag (MouseFlags.Button1DoubleClicked | MouseFlags.ButtonShift)
+ && !ev.Flags.HasFlag (ContextMenu!.MouseFlags))
+ {
+ return false;
+ }
+
+ if (!CanFocus)
+ {
+ return true;
+ }
+
+ if (!HasFocus)
+ {
+ SetFocus ();
+ }
+
+ _continuousFind = false;
+
+ // Give autocomplete first opportunity to respond to mouse clicks
+ if (SelectedLength == 0 && Autocomplete.OnMouseEvent (ev, true))
+ {
+ return true;
+ }
+
+ if (ev.Flags == MouseFlags.Button1Clicked)
+ {
+ if (_isButtonReleased)
+ {
+ _isButtonReleased = false;
+
+ if (SelectedLength == 0)
+ {
+ StopSelecting ();
+ }
+
+ return true;
+ }
+
+ if (_shiftSelecting && !_isButtonShift)
+ {
+ StopSelecting ();
+ }
+
+ ProcessMouseClick (ev, out _);
+
+ if (Used)
+ {
+ PositionCursor ();
+ }
+ else
+ {
+ SetNeedsDraw ();
+ }
+
+ _lastWasKill = false;
+ _columnTrack = CurrentColumn;
+ }
+ else if (ev.Flags == MouseFlags.WheeledDown)
+ {
+ _lastWasKill = false;
+ _columnTrack = CurrentColumn;
+ ScrollTo (_topRow + 1);
+ }
+ else if (ev.Flags == MouseFlags.WheeledUp)
+ {
+ _lastWasKill = false;
+ _columnTrack = CurrentColumn;
+ ScrollTo (_topRow - 1);
+ }
+ else if (ev.Flags == MouseFlags.WheeledRight)
+ {
+ _lastWasKill = false;
+ _columnTrack = CurrentColumn;
+ ScrollTo (_leftColumn + 1, false);
+ }
+ else if (ev.Flags == MouseFlags.WheeledLeft)
+ {
+ _lastWasKill = false;
+ _columnTrack = CurrentColumn;
+ ScrollTo (_leftColumn - 1, false);
+ }
+ else if (ev.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition))
+ {
+ ProcessMouseClick (ev, out List line);
+ PositionCursor ();
+
+ if (_model.Count > 0 && _shiftSelecting && IsSelecting)
+ {
+ if (CurrentRow - _topRow >= Viewport.Height - 1 && _model.Count > _topRow + CurrentRow)
+ {
+ ScrollTo (_topRow + Viewport.Height);
+ }
+ else if (_topRow > 0 && CurrentRow <= _topRow)
+ {
+ ScrollTo (_topRow - Viewport.Height);
+ }
+ else if (ev.Position.Y >= Viewport.Height)
+ {
+ ScrollTo (_model.Count);
+ }
+ else if (ev.Position.Y < 0 && _topRow > 0)
+ {
+ ScrollTo (0);
+ }
+
+ if (CurrentColumn - _leftColumn >= Viewport.Width - 1 && line.Count > _leftColumn + CurrentColumn)
+ {
+ ScrollTo (_leftColumn + Viewport.Width, false);
+ }
+ else if (_leftColumn > 0 && CurrentColumn <= _leftColumn)
+ {
+ ScrollTo (_leftColumn - Viewport.Width, false);
+ }
+ else if (ev.Position.X >= Viewport.Width)
+ {
+ ScrollTo (line.Count, false);
+ }
+ else if (ev.Position.X < 0 && _leftColumn > 0)
+ {
+ ScrollTo (0, false);
+ }
+ }
+
+ _lastWasKill = false;
+ _columnTrack = CurrentColumn;
+ }
+ else if (ev.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ButtonShift))
+ {
+ if (!_shiftSelecting)
+ {
+ _isButtonShift = true;
+ StartSelecting ();
+ }
+
+ ProcessMouseClick (ev, out _);
+ PositionCursor ();
+ _lastWasKill = false;
+ _columnTrack = CurrentColumn;
+ }
+ else if (ev.Flags.HasFlag (MouseFlags.Button1Pressed))
+ {
+ if (_shiftSelecting)
+ {
+ _clickWithSelecting = true;
+ StopSelecting ();
+ }
+
+ ProcessMouseClick (ev, out _);
+ PositionCursor ();
+
+ if (!IsSelecting)
+ {
+ StartSelecting ();
+ }
+
+ _lastWasKill = false;
+ _columnTrack = CurrentColumn;
+
+ if (App?.Mouse.MouseGrabView is null)
+ {
+ App?.Mouse.GrabMouse (this);
+ }
+ }
+ else if (ev.Flags.HasFlag (MouseFlags.Button1Released))
+ {
+ _isButtonReleased = true;
+ App?.Mouse.UngrabMouse ();
+ }
+ else if (ev.Flags.HasFlag (MouseFlags.Button1DoubleClicked))
+ {
+ if (ev.Flags.HasFlag (MouseFlags.ButtonShift))
+ {
+ if (!IsSelecting)
+ {
+ StartSelecting ();
+ }
+ }
+ else if (IsSelecting)
+ {
+ StopSelecting ();
+ }
+
+ ProcessMouseClick (ev, out List line);
+
+ if (!IsSelecting)
+ {
+ StartSelecting ();
+ }
+
+ (int startCol, int col, int row)? newPos = _model.ProcessDoubleClickSelection (SelectionStartColumn, CurrentColumn, CurrentRow, UseSameRuneTypeForWords, SelectWordOnlyOnDoubleClick);
+
+ if (newPos.HasValue)
+ {
+ SelectionStartColumn = newPos.Value.startCol;
+ CurrentColumn = newPos.Value.col;
+ CurrentRow = newPos.Value.row;
+ }
+
+ PositionCursor ();
+ _lastWasKill = false;
+ _columnTrack = CurrentColumn;
+ SetNeedsDraw ();
+ }
+ else if (ev.Flags.HasFlag (MouseFlags.Button1TripleClicked))
+ {
+ if (IsSelecting)
+ {
+ StopSelecting ();
+ }
+
+ ProcessMouseClick (ev, out List line);
+ CurrentColumn = 0;
+
+ if (!IsSelecting)
+ {
+ StartSelecting ();
+ }
+
+ CurrentColumn = line.Count;
+ PositionCursor ();
+ _lastWasKill = false;
+ _columnTrack = CurrentColumn;
+ SetNeedsDraw ();
+ }
+ else if (ev.Flags == ContextMenu!.MouseFlags)
+ {
+ ShowContextMenu (ev.ScreenPosition);
+ }
+
+ OnUnwrappedCursorPosition ();
+
+ return true;
+ }
+
+ ///
+ /// Called when the contents of the TextView change. E.g. when the user types text or deletes text. Raises the
+ /// event.
+ ///
+ public virtual void OnContentsChanged ()
+ {
+ ContentsChanged?.Invoke (this, new (CurrentRow, CurrentColumn));
+
+ ProcessInheritsPreviousScheme (CurrentRow, CurrentColumn);
+ ProcessAutocomplete ();
+
+ // Update content size when content changes
+ if (IsInitialized)
+ {
+ UpdateContentSize ();
+ }
+ }
+
+ ///
+ protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? view)
+ {
+ if (App?.Mouse.MouseGrabView is { } && App?.Mouse.MouseGrabView == this)
+ {
+ App?.Mouse.UngrabMouse ();
+ }
+ }
+
+ ///
+ protected override bool OnKeyDown (Key key)
+ {
+ if (!key.IsValid)
+ {
+ return false;
+ }
+
+ // Give autocomplete first opportunity to respond to key presses
+ if (SelectedLength == 0 && Autocomplete.Suggestions.Count > 0 && Autocomplete.ProcessKey (key))
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ protected override bool OnKeyDownNotHandled (Key a)
+ {
+ if (!CanFocus)
+ {
+ return true;
+ }
+
+ ResetColumnTrack ();
+
+ // Ignore control characters and other special keys
+ if (!a.IsKeyCodeAtoZ && (a.KeyCode < KeyCode.Space || a.KeyCode > KeyCode.CharMask))
+ {
+ return false;
+ }
+
+ InsertText (a);
+ DoNeededAction ();
+
+ return true;
+ }
+
+ ///
+ public override bool OnKeyUp (Key key)
+ {
+ if (key == Key.Space.WithCtrl)
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ /// Positions the cursor on the current row and column
+ public override Point? PositionCursor ()
+ {
+ ProcessAutocomplete ();
+
+ if (!CanFocus || !Enabled || Driver is null)
+ {
+ return null;
+ }
+
+ if (App?.Mouse.MouseGrabView == this && IsSelecting)
+ {
+ // BUGBUG: customized rect aren't supported now because the Redraw isn't using the Intersect method.
+ //var minRow = Math.Min (Math.Max (Math.Min (selectionStartRow, currentRow) - topRow, 0), Viewport.Height);
+ //var maxRow = Math.Min (Math.Max (Math.Max (selectionStartRow, currentRow) - topRow, 0), Viewport.Height);
+ //SetNeedsDraw (new (0, minRow, Viewport.Width, maxRow));
+ SetNeedsDraw ();
+ }
+
+ List line = _model.GetLine (CurrentRow);
+ var col = 0;
+
+ if (line.Count > 0)
+ {
+ for (int idx = _leftColumn; idx < line.Count; idx++)
+ {
+ if (idx >= CurrentColumn)
+ {
+ break;
+ }
+
+ int cols = line [idx].Grapheme.GetColumns ();
+
+ if (line [idx].Grapheme == "\t")
+ {
+ cols += TabWidth + 1;
+ }
+ else
+ {
+ // Ensures that cols less than 0 to be 1 because it will be converted to a printable rune
+ cols = Math.Max (cols, 1);
+ }
+
+ if (!TextModel.SetCol (ref col, Viewport.Width, cols))
+ {
+ col = CurrentColumn;
+
+ break;
+ }
+ }
+ }
+
+ int posX = CurrentColumn - _leftColumn;
+ int posY = CurrentRow - _topRow;
+
+ if (posX > -1 && col >= posX && posX < Viewport.Width && _topRow <= CurrentRow && posY < Viewport.Height)
+ {
+ Move (col, CurrentRow - _topRow);
+
+ return new (col, CurrentRow - _topRow);
+ }
+
+ return null; // Hide cursor
+ }
+
+ /// Redoes the latest changes.
+ public void Redo ()
+ {
+ if (ReadOnly)
+ {
+ return;
+ }
+
+ _historyText.Redo ();
+ }
+
+ ///// Raised when the property of the changes.
+ /////
+ ///// The property of only changes when it is explicitly set, not as the
+ ///// user types. To be notified as the user changes the contents of the TextView see .
+ /////
+ //public event EventHandler? TextChanged;
+
+ /// Undoes the latest changes.
+ public void Undo ()
+ {
+ if (ReadOnly)
+ {
+ return;
+ }
+
+ _historyText.Undo ();
+ }
+
+ private void ClearRegion (int left, int top, int right, int bottom)
+ {
+ for (int row = top; row < bottom; row++)
+ {
+ Move (left, row);
+
+ for (int col = left; col < right; col++)
+ {
+ AddRune (col, row, (Rune)' ');
+ }
+ }
+ }
+
+ private void GenerateSuggestions ()
+ {
+ List currentLine = GetCurrentLine ();
+ int cursorPosition = Math.Min (CurrentColumn, currentLine.Count);
+
+ Autocomplete.Context = new (
+ currentLine,
+ cursorPosition,
+ Autocomplete.Context != null
+ ? Autocomplete.Context.Canceled
+ : false
+ );
+
+ Autocomplete.GenerateSuggestions (
+ Autocomplete.Context
+ );
+ }
+
+ private void ProcessAutocomplete ()
+ {
+ if (_isDrawing)
+ {
+ return;
+ }
+
+ if (_clickWithSelecting)
+ {
+ _clickWithSelecting = false;
+
+ return;
+ }
+
+ if (SelectedLength > 0)
+ {
+ return;
+ }
+
+ // draw autocomplete
+ GenerateSuggestions ();
+
+ var renderAt = new Point (
+ Autocomplete.Context.CursorPosition,
+ Autocomplete.PopupInsideContainer
+ ? CursorPosition.Y + 1 - TopRow
+ : 0
+ );
+
+ Autocomplete.RenderOverlay (renderAt);
+ }
+
+ private bool ProcessBackTab ()
+ {
+ ResetColumnTrack ();
+
+ if (!AllowsTab || _isReadOnly)
+ {
+ return false;
+ }
+
+ if (CurrentColumn > 0)
+ {
+ SetWrapModel ();
+
+ List currentLine = GetCurrentLine ();
+
+ if (currentLine.Count > 0 && currentLine [CurrentColumn - 1].Grapheme == "\t")
+ {
+ _historyText.Add (new () { new (currentLine) }, CursorPosition);
+
+ currentLine.RemoveAt (CurrentColumn - 1);
+ CurrentColumn--;
+
+ _historyText.Add (
+ new () { new (GetCurrentLine ()) },
+ CursorPosition,
+ TextEditingLineStatus.Replaced
+ );
+ }
+
+ SetNeedsDraw ();
+
+ UpdateWrapModel ();
+ }
+
+ DoNeededAction ();
+
+ return true;
+ }
+
+ // If InheritsPreviousScheme is enabled this method will check if the rune cell on
+ // the row and col location and around has a not null scheme. If it's null will set it with
+ // the very most previous valid scheme.
+ private void ProcessInheritsPreviousScheme (int row, int col)
+ {
+ if (!InheritsPreviousAttribute || (Lines == 1 && GetLine (Lines).Count == 0))
+ {
+ return;
+ }
+
+ List line = GetLine (row);
+ List lineToSet = line;
+
+ while (line.Count == 0)
+ {
+ if (row == 0 && line.Count == 0)
+ {
+ return;
+ }
+
+ row--;
+ line = GetLine (row);
+ lineToSet = line;
+ }
+
+ int colWithColor = Math.Max (Math.Min (col - 2, line.Count - 1), 0);
+ Cell cell = line [colWithColor];
+ int colWithoutColor = Math.Max (col - 1, 0);
+
+ Cell lineTo = lineToSet [colWithoutColor];
+
+ if (cell.Attribute is { } && colWithColor == 0 && lineTo.Attribute is { })
+ {
+ for (int r = row - 1; r > -1; r--)
+ {
+ List l = GetLine (r);
+
+ for (int c = l.Count - 1; c > -1; c--)
+ {
+ Cell cell1 = l [c];
+
+ if (cell1.Attribute is null)
+ {
+ cell1.Attribute = cell.Attribute;
+ l [c] = cell1;
+ }
+ else
+ {
+ return;
+ }
+ }
+ }
+
+ return;
+ }
+
+ if (cell.Attribute is null)
+ {
+ for (int r = row; r > -1; r--)
+ {
+ List l = GetLine (r);
+
+ colWithColor = l.FindLastIndex (
+ colWithColor > -1 ? colWithColor : l.Count - 1,
+ c => c.Attribute != null
+ );
+
+ if (colWithColor > -1 && l [colWithColor].Attribute is { })
+ {
+ cell = l [colWithColor];
+
+ break;
+ }
+ }
+ }
+ else
+ {
+ int cRow = row;
+
+ while (cell.Attribute is null)
+ {
+ if ((colWithColor == 0 || cell.Attribute is null) && cRow > 0)
+ {
+ line = GetLine (--cRow);
+ colWithColor = line.Count - 1;
+ cell = line [colWithColor];
+ }
+ else if (cRow == 0 && colWithColor < line.Count)
+ {
+ cell = line [colWithColor + 1];
+ }
+ }
+ }
+
+ if (cell.Attribute is { } && colWithColor > -1 && colWithoutColor < lineToSet.Count && lineTo.Attribute is null)
+ {
+ while (lineTo.Attribute is null)
+ {
+ lineTo.Attribute = cell.Attribute;
+ lineToSet [colWithoutColor] = lineTo;
+ colWithoutColor--;
+
+ if (colWithoutColor == -1 && row > 0)
+ {
+ lineToSet = GetLine (--row);
+ colWithoutColor = lineToSet.Count - 1;
+ }
+ }
+ }
+ }
+
+ private void ProcessMouseClick (MouseEventArgs ev, out List line)
+ {
+ List? r = null;
+
+ if (_model.Count > 0)
+ {
+ int maxCursorPositionableLine = Math.Max (_model.Count - 1 - _topRow, 0);
+
+ if (Math.Max (ev.Position.Y, 0) > maxCursorPositionableLine)
+ {
+ CurrentRow = maxCursorPositionableLine + _topRow;
+ }
+ else
+ {
+ CurrentRow = Math.Max (ev.Position.Y + _topRow, 0);
+ }
+
+ r = GetCurrentLine ();
+ int idx = TextModel.GetColFromX (r, _leftColumn, Math.Max (ev.Position.X, 0), TabWidth);
+
+ if (idx - _leftColumn >= r.Count)
+ {
+ CurrentColumn = Math.Max (r.Count - _leftColumn - (ReadOnly ? 1 : 0), 0);
+ }
+ else
+ {
+ CurrentColumn = idx + _leftColumn;
+ }
+ }
+
+ line = r!;
+ }
+
+
+
+ private bool ProcessEnterKey (ICommandContext? commandContext)
+ {
+ ResetColumnTrack ();
+
+ if (_isReadOnly)
+ {
+ return false;
+ }
+
+ if (!AllowsReturn)
+ {
+ // By Default pressing ENTER should be ignored (OnAccept will return false or null). Only cancel if the
+ // event was fired and set Cancel = true.
+ return RaiseAccepting (commandContext) is null or false;
+ }
+
+ SetWrapModel ();
+
+ List currentLine = GetCurrentLine ();
+
+ _historyText.Add (new () { new (currentLine) }, CursorPosition);
+
+ if (IsSelecting)
+ {
+ ClearSelectedRegion ();
+ currentLine = GetCurrentLine ();
+ }
+
+ int restCount = currentLine.Count - CurrentColumn;
+ List rest = currentLine.GetRange (CurrentColumn, restCount);
+ currentLine.RemoveRange (CurrentColumn, restCount);
+
+ List> addedLines = new () { new (currentLine) };
+
+ _model.AddLine (CurrentRow + 1, rest);
+
+ addedLines.Add (new (_model.GetLine (CurrentRow + 1)));
+
+ _historyText.Add (addedLines, CursorPosition, TextEditingLineStatus.Added);
+
+ CurrentRow++;
+
+ var fullNeedsDraw = false;
+
+ if (CurrentRow >= _topRow + Viewport.Height)
+ {
+ _topRow++;
+ fullNeedsDraw = true;
+ }
+
+ CurrentColumn = 0;
+
+ _historyText.Add (
+ new () { new (GetCurrentLine ()) },
+ CursorPosition,
+ TextEditingLineStatus.Replaced
+ );
+
+ if (!_wordWrap && CurrentColumn < _leftColumn)
+ {
+ fullNeedsDraw = true;
+ _leftColumn = 0;
+ }
+
+ if (fullNeedsDraw)
+ {
+ SetNeedsDraw ();
+ }
+ else
+ {
+ // BUGBUG: customized rect aren't supported now because the Redraw isn't using the Intersect method.
+ //SetNeedsDraw (new (0, currentRow - topRow, 2, Viewport.Height));
+ SetNeedsDraw ();
+ }
+
+ UpdateWrapModel ();
+
+ DoNeededAction ();
+ OnContentsChanged ();
+
+ return true;
+ }
+
+ private void ProcessSetOverwrite ()
+ {
+ ResetColumnTrack ();
+ SetOverwrite (!Used);
+ }
+
+ private bool ProcessTab ()
+ {
+ ResetColumnTrack ();
+
+ if (!AllowsTab || _isReadOnly)
+ {
+ return false;
+ }
+
+ InsertText (new Key ((KeyCode)'\t'));
+ DoNeededAction ();
+
+ return true;
+ }
+
+
+
+ private void SetOverwrite (bool overwrite)
+ {
+ Used = overwrite;
+ SetNeedsDraw ();
+ DoNeededAction ();
+ }
+
+ private void SetValidUsedColor (Attribute? attribute)
+ {
+ // BUGBUG: (v2 truecolor) This code depends on 8-bit color names; disabling for now
+ //if ((scheme!.HotNormal.Foreground & scheme.Focus.Background) == scheme.Focus.Foreground) {
+ SetAttribute (new (attribute!.Value.Background, attribute!.Value.Foreground, attribute!.Value.Style));
+ }
+
+}
\ No newline at end of file
diff --git a/Terminal.Gui/Views/TextInput/TextView/TextViewAutocomplete.cs b/Terminal.Gui/Views/TextInput/TextView/TextViewAutocomplete.cs
new file mode 100644
index 0000000000..5e5a672448
--- /dev/null
+++ b/Terminal.Gui/Views/TextInput/TextView/TextViewAutocomplete.cs
@@ -0,0 +1,21 @@
+namespace Terminal.Gui.Views;
+
+///
+/// Renders an overlay on another view at a given point that allows selecting from a range of 'autocomplete'
+/// options. An implementation on a TextView.
+///
+public class TextViewAutocomplete : PopupAutocomplete
+{
+ ///
+ protected override void DeleteTextBackwards () { ((TextView)HostControl!).DeleteCharLeft (); }
+
+ ///
+ protected override void InsertText (string accepted) { ((TextView)HostControl!).InsertText (accepted); }
+
+ ///
+ protected override void SetCursorPosition (int column)
+ {
+ ((TextView)HostControl!).CursorPosition =
+ new (column, ((TextView)HostControl).CurrentRow);
+ }
+}
diff --git a/Terminal.Gui/Views/TextInput/WordWrapManager.cs b/Terminal.Gui/Views/TextInput/TextView/WordWrapManager.cs
similarity index 100%
rename from Terminal.Gui/Views/TextInput/WordWrapManager.cs
rename to Terminal.Gui/Views/TextInput/TextView/WordWrapManager.cs
diff --git a/Terminal.sln.DotSettings b/Terminal.sln.DotSettings
index ef25662a47..0662bf5a6f 100644
--- a/Terminal.sln.DotSettings
+++ b/Terminal.sln.DotSettings
@@ -1,7 +1,9 @@

BackingField
Inherit
+ ReturnDefaultValue
True
+ True
5000
1000
3000
@@ -14,7 +16,7 @@
SUGGESTION
WARNING
ERROR
- ERROR
+ SUGGESTION
WARNING
SUGGESTION
WARNING
@@ -331,6 +333,7 @@
<Entry.SortBy>
<Access Is="0" />
<Readonly />
+ <PropertyName />
</Entry.SortBy>
</Entry>
<Property DisplayName="Properties w/ Backing Field" Priority="100">
@@ -353,14 +356,18 @@
</And>
</Entry.Match>
<Entry.SortBy>
- <ImplementsInterface Immediate="True" />
+ <ImplementsInterface />
+ <Name />
</Entry.SortBy>
</Entry>
<Entry DisplayName="All other members">
<Entry.SortBy>
<Access Is="0" />
- <Name />
+ <Static />
+ <Virtual />
<Override />
+ <ImplementsInterface />
+ <Name />
</Entry.SortBy>
</Entry>
<Entry DisplayName="Nested Types">
@@ -374,10 +381,11 @@
</Entry>
</TypePattern>
</Patterns>
- UseVarWhenEvident
+ UseExplicitType
UseExplicitType
- UseVarWhenEvident
+ UseVar
True
+ True
False
False
True
diff --git a/Tests/UnitTests/Views/TextViewTests.cs b/Tests/UnitTests/Views/TextViewTests.cs
index 10764cf4cf..fb060934d4 100644
--- a/Tests/UnitTests/Views/TextViewTests.cs
+++ b/Tests/UnitTests/Views/TextViewTests.cs
@@ -55,65 +55,6 @@ public void AllowsTab_Setting_To_True_Changes_TabWidth_To_Default_If_It_Is_Zero
Assert.True (_textView.AllowsReturn);
Assert.True (_textView.Multiline);
}
-
- [Fact]
- [TextViewTestsSetupFakeApplication]
- public void BackTab_Test_Follow_By_Tab ()
- {
- var top = new Toplevel ();
- top.Add (_textView);
-
- Application.Iteration += OnApplicationOnIteration;
-
- Application.Run (top);
- Application.Iteration -= OnApplicationOnIteration;
- top.Dispose ();
-
- return;
-
- void OnApplicationOnIteration (object s, IterationEventArgs a)
- {
- int width = _textView.Viewport.Width - 1;
- Assert.Equal (30, width + 1);
- Assert.Equal (10, _textView.Height);
- _textView.Text = "";
-
- for (var i = 0; i < 100; i++)
- {
- _textView.Text += "\t";
- }
-
- var col = 100;
- int tabWidth = _textView.TabWidth;
- int leftCol = _textView.LeftColumn;
- _textView.MoveEnd ();
- Assert.Equal (new (col, 0), _textView.CursorPosition);
- leftCol = GetLeftCol (leftCol);
- Assert.Equal (leftCol, _textView.LeftColumn);
-
- while (col > 0)
- {
- col--;
- _textView.NewKeyDownEvent (Key.Tab.WithShift);
- Assert.Equal (new (col, 0), _textView.CursorPosition);
- leftCol = GetLeftCol (leftCol);
- Assert.Equal (leftCol, _textView.LeftColumn);
- }
-
- while (col < 100)
- {
- col++;
- _textView.NewKeyDownEvent (Key.Tab);
- Assert.Equal (new (col, 0), _textView.CursorPosition);
- leftCol = GetLeftCol (leftCol);
- Assert.Equal (leftCol, _textView.LeftColumn);
- }
-
- Application.TopRunnable.Remove (_textView);
- Application.RequestStop ();
- }
- }
-
[Fact]
[TextViewTestsSetupFakeApplication]
public void CanFocus_False_Wont_Focus_With_Mouse ()
@@ -3406,829 +3347,110 @@ public void HistoryText_Undo_Redo_Single_Line_Selected_InsertText ()
Assert.Equal (0, tv.SelectedLength);
top.Dispose ();
}
-
[Fact]
- [SetupFakeApplication]
- public void KeyBindings_Command ()
+ [TextViewTestsSetupFakeApplication]
+ public void Kill_Delete_WordBackward ()
{
- var text = "This is the first line.\nThis is the second line.\nThis is the third line.";
- var tv = new TextView { Width = 10, Height = 2, Text = text };
- Toplevel top = new ();
- top.Add (tv);
- Application.Begin (top);
+ _textView.Text = "This is the first line.";
+ _textView.MoveEnd ();
+ var iteration = 0;
+ var iterationsFinished = false;
- Assert.Equal (
- $"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.",
- tv.Text
- );
- Assert.Equal (3, tv.Lines);
- Assert.Equal (Point.Empty, tv.CursorPosition);
- Assert.False (tv.ReadOnly);
- Assert.True (tv.CanFocus);
- Assert.False (tv.IsSelecting);
+ while (!iterationsFinished)
+ {
+ _textView.NewKeyDownEvent (Key.Backspace.WithCtrl);
- var g = (SingleWordSuggestionGenerator)tv.Autocomplete.SuggestionGenerator;
+ switch (iteration)
+ {
+ case 0:
+ Assert.Equal (22, _textView.CursorPosition.X);
+ Assert.Equal (0, _textView.CursorPosition.Y);
+ Assert.Equal ("This is the first line", _textView.Text);
- tv.CanFocus = false;
- Assert.True (tv.NewKeyDownEvent (Key.CursorLeft));
- Assert.False (tv.IsSelecting);
- tv.CanFocus = true;
- Assert.False (tv.NewKeyDownEvent (Key.CursorLeft));
- Assert.False (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.CursorRight));
- Assert.Equal (new (1, 0), tv.CursorPosition);
- Assert.False (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.End.WithCtrl));
- Assert.Equal (2, tv.CurrentRow);
- Assert.Equal (23, tv.CurrentColumn);
- Assert.Equal (tv.CurrentColumn, tv.GetCurrentLine ().Count);
- Assert.Equal (new (23, 2), tv.CursorPosition);
- Assert.False (tv.IsSelecting);
- Assert.False (tv.NewKeyDownEvent (Key.CursorRight));
- Assert.NotNull (tv.Autocomplete);
- Assert.Empty (g.AllSuggestions);
- Assert.False (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.F.WithShift));
- tv.Draw ();
+ break;
+ case 1:
+ Assert.Equal (18, _textView.CursorPosition.X);
+ Assert.Equal (0, _textView.CursorPosition.Y);
+ Assert.Equal ("This is the first ", _textView.Text);
- Assert.Equal (
- $"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.F",
- tv.Text
- );
- Assert.Equal (new (24, 2), tv.CursorPosition);
- Assert.Empty (tv.Autocomplete.Suggestions);
- Assert.False (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.Z.WithCtrl));
- tv.Draw ();
+ break;
+ case 2:
+ Assert.Equal (12, _textView.CursorPosition.X);
+ Assert.Equal (0, _textView.CursorPosition.Y);
+ Assert.Equal ("This is the ", _textView.Text);
- Assert.Equal (
- $"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.",
- tv.Text
- );
- Assert.Equal (new (23, 2), tv.CursorPosition);
- Assert.Empty (tv.Autocomplete.Suggestions);
- Assert.False (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.R.WithCtrl));
- tv.Draw ();
+ break;
+ case 3:
+ Assert.Equal (8, _textView.CursorPosition.X);
+ Assert.Equal (0, _textView.CursorPosition.Y);
+ Assert.Equal ("This is ", _textView.Text);
- Assert.Equal (
- $"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.F",
- tv.Text
- );
- Assert.Equal (new (24, 2), tv.CursorPosition);
- Assert.Empty (tv.Autocomplete.Suggestions);
- Assert.False (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.Backspace));
+ break;
+ case 4:
+ Assert.Equal (5, _textView.CursorPosition.X);
+ Assert.Equal (0, _textView.CursorPosition.Y);
+ Assert.Equal ("This ", _textView.Text);
- Assert.Equal (
- $"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.",
- tv.Text
- );
- Assert.Equal (new (23, 2), tv.CursorPosition);
-
- g.AllSuggestions = Regex.Matches (tv.Text, "\\w+")
- .Select (s => s.Value)
- .Distinct ()
- .ToList ();
- Assert.Equal (7, g.AllSuggestions.Count);
- Assert.Equal ("This", g.AllSuggestions [0]);
- Assert.Equal ("is", g.AllSuggestions [1]);
- Assert.Equal ("the", g.AllSuggestions [2]);
- Assert.Equal ("first", g.AllSuggestions [3]);
- Assert.Equal ("line", g.AllSuggestions [4]);
- Assert.Equal ("second", g.AllSuggestions [5]);
- Assert.Equal ("third", g.AllSuggestions [^1]);
- Assert.False (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.F.WithShift));
- tv.Draw ();
+ break;
+ case 5:
+ Assert.Equal (0, _textView.CursorPosition.X);
+ Assert.Equal (0, _textView.CursorPosition.Y);
+ Assert.Equal ("", _textView.Text);
- Assert.Equal (
- $"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.F",
- tv.Text
- );
- Assert.Equal (new (24, 2), tv.CursorPosition);
- Assert.Single (tv.Autocomplete.Suggestions);
- Assert.Equal ("first", tv.Autocomplete.Suggestions [0].Replacement);
- Assert.False (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.Enter));
+ break;
+ default:
+ iterationsFinished = true;
- Assert.Equal (
- $"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first",
- tv.Text
- );
- Assert.Equal (new (28, 2), tv.CursorPosition);
- Assert.Empty (tv.Autocomplete.Suggestions);
- Assert.False (tv.Autocomplete.Visible);
- g.AllSuggestions = new ();
- tv.Autocomplete.ClearSuggestions ();
- Assert.Empty (g.AllSuggestions);
- Assert.Empty (tv.Autocomplete.Suggestions);
- Assert.False (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.PageUp));
- Assert.Equal (24, tv.GetCurrentLine ().Count);
- Assert.Equal (new (24, 1), tv.CursorPosition);
- Assert.False (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (new (Key.PageUp)));
- Assert.Equal (23, tv.GetCurrentLine ().Count);
- Assert.Equal (new (23, 0), tv.CursorPosition);
- Assert.False (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.PageDown));
- Assert.Equal (24, tv.GetCurrentLine ().Count);
- Assert.Equal (new (23, 1), tv.CursorPosition); // gets the previous length
- Assert.False (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.V.WithCtrl));
- Assert.Equal (28, tv.GetCurrentLine ().Count);
- Assert.Equal (new (23, 2), tv.CursorPosition); // gets the previous length
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.False (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.PageUp.WithShift));
- Assert.Equal (24, tv.GetCurrentLine ().Count);
- Assert.Equal (new (23, 1), tv.CursorPosition); // gets the previous length
- Assert.Equal (24 + Environment.NewLine.Length, tv.SelectedLength);
- Assert.Equal ($".{Environment.NewLine}This is the third line.", tv.SelectedText);
- Assert.True (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.PageDown.WithShift));
- Assert.Equal (28, tv.GetCurrentLine ().Count);
- Assert.Equal (new (23, 2), tv.CursorPosition); // gets the previous length
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.True (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.Home.WithCtrl));
- Assert.Equal (Point.Empty, tv.CursorPosition);
- Assert.False (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.N.WithCtrl));
- Assert.Equal (new (0, 1), tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.False (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.P.WithCtrl));
- Assert.Equal (Point.Empty, tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.False (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.CursorDown));
- Assert.Equal (new (0, 1), tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.False (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.CursorUp));
- Assert.Equal (Point.Empty, tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.False (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.CursorDown.WithShift));
- Assert.Equal (new (0, 1), tv.CursorPosition);
- Assert.Equal (23 + Environment.NewLine.Length, tv.SelectedLength);
- Assert.Equal ($"This is the first line.{Environment.NewLine}", tv.SelectedText);
- Assert.True (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.CursorUp.WithShift));
- Assert.Equal (Point.Empty, tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.True (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.F.WithCtrl));
- Assert.Equal (new (1, 0), tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.False (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.B.WithCtrl));
- Assert.Equal (Point.Empty, tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.False (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.CursorRight));
- Assert.Equal (new (1, 0), tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.False (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.CursorLeft));
- Assert.Equal (Point.Empty, tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.False (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.CursorRight.WithShift));
- Assert.Equal (new (1, 0), tv.CursorPosition);
- Assert.Equal (1, tv.SelectedLength);
- Assert.Equal ("T", tv.SelectedText);
- Assert.True (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.CursorLeft.WithShift));
- Assert.Equal (Point.Empty, tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.True (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.Delete));
+ break;
+ }
- Assert.Equal (
- $"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first",
- tv.Text
- );
- Assert.Equal (Point.Empty, tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.False (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.Delete));
+ iteration++;
+ }
+ }
- Assert.Equal (
- $"his is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first",
- tv.Text
- );
- Assert.Equal (Point.Empty, tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.False (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.D.WithCtrl));
+ [Fact]
+ [TextViewTestsSetupFakeApplication]
+ public void Kill_Delete_WordBackward_Multiline ()
+ {
+ _textView.Text = "This is the first line.\nThis is the second line.";
+ _textView.Width = 4;
+ _textView.MoveEnd ();
+ var iteration = 0;
+ var iterationsFinished = false;
- Assert.Equal (
- $"is is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first",
- tv.Text
- );
- Assert.Equal (Point.Empty, tv.CursorPosition);
- Assert.True (tv.NewKeyDownEvent (Key.End));
+ while (!iterationsFinished)
+ {
+ _textView.NewKeyDownEvent (Key.Backspace.WithCtrl);
- Assert.Equal (
- $"is is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first",
- tv.Text
- );
- Assert.Equal (new (21, 0), tv.CursorPosition);
- Assert.False (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.Backspace));
+ switch (iteration)
+ {
+ case 0:
+ Assert.Equal (23, _textView.CursorPosition.X);
+ Assert.Equal (1, _textView.CursorPosition.Y);
- Assert.Equal (
- $"is is the first line{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first",
- tv.Text
- );
- Assert.Equal (new (20, 0), tv.CursorPosition);
- Assert.False (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.Backspace));
+ Assert.Equal (
+ "This is the first line."
+ + Environment.NewLine
+ + "This is the second line",
+ _textView.Text
+ );
- Assert.Equal (
- $"is is the first lin{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first",
- tv.Text
- );
- Assert.Equal (new (19, 0), tv.CursorPosition);
- Assert.False (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.Home));
- Assert.Equal (Point.Empty, tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.False (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.End.WithShift));
- Assert.Equal (new (19, 0), tv.CursorPosition);
- Assert.Equal (19, tv.SelectedLength);
- Assert.Equal ("is is the first lin", tv.SelectedText);
- Assert.True (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.Home.WithShift));
- Assert.Equal (Point.Empty, tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.True (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.E.WithCtrl));
- Assert.Equal (new (19, 0), tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.False (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.Home));
- Assert.Equal (Point.Empty, tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.False (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.K.WithCtrl));
+ break;
+ case 1:
+ Assert.Equal (19, _textView.CursorPosition.X);
+ Assert.Equal (1, _textView.CursorPosition.Y);
- Assert.Equal (
- $"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first",
- tv.Text
- );
- Assert.Equal (Point.Empty, tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.False (tv.IsSelecting);
- Assert.Equal ("is is the first lin", Clipboard.Contents);
- Assert.True (tv.NewKeyDownEvent (Key.Y.WithCtrl));
+ Assert.Equal (
+ "This is the first line."
+ + Environment.NewLine
+ + "This is the second ",
+ _textView.Text
+ );
- Assert.Equal (
- $"is is the first lin{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first",
- tv.Text
- );
- Assert.Equal (new (19, 0), tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.False (tv.IsSelecting);
- Assert.Equal ("is is the first lin", Clipboard.Contents);
- tv.CursorPosition = Point.Empty;
- Assert.True (tv.NewKeyDownEvent (Key.Delete.WithCtrl.WithShift));
-
- Assert.Equal (
- $"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first",
- tv.Text
- );
- Assert.Equal (Point.Empty, tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.False (tv.IsSelecting);
- Assert.Equal ("is is the first lin", Clipboard.Contents);
- Assert.True (tv.NewKeyDownEvent (Key.Y.WithCtrl));
-
- Assert.Equal (
- $"is is the first lin{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first",
- tv.Text
- );
- Assert.Equal (new (19, 0), tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.False (tv.IsSelecting);
- Assert.Equal ("is is the first lin", Clipboard.Contents);
- Assert.True (tv.NewKeyDownEvent (Key.Backspace.WithCtrl.WithShift));
-
- Assert.Equal (
- $"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first",
- tv.Text
- );
- Assert.Equal (Point.Empty, tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.False (tv.IsSelecting);
- tv.ReadOnly = true;
- Assert.True (tv.NewKeyDownEvent (Key.Y.WithCtrl));
-
- Assert.Equal (
- $"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first",
- tv.Text
- );
- Assert.Equal (Point.Empty, tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.False (tv.IsSelecting);
- tv.ReadOnly = false;
- Assert.True (tv.NewKeyDownEvent (Key.Y.WithCtrl));
-
- Assert.Equal (
- $"is is the first lin{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first",
- tv.Text
- );
- Assert.Equal (new (19, 0), tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.False (tv.IsSelecting);
- Assert.Equal (0, tv.SelectionStartColumn);
- Assert.Equal (0, tv.SelectionStartRow);
- Assert.True (tv.NewKeyDownEvent (Key.Space.WithCtrl));
-
- Assert.Equal (
- $"is is the first lin{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first",
- tv.Text
- );
- Assert.Equal (new (19, 0), tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.True (tv.IsSelecting);
- Assert.Equal (19, tv.SelectionStartColumn);
- Assert.Equal (0, tv.SelectionStartRow);
- Assert.True (tv.NewKeyDownEvent (Key.Space.WithCtrl));
-
- Assert.Equal (
- $"is is the first lin{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first",
- tv.Text
- );
- Assert.Equal (new (19, 0), tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.False (tv.IsSelecting);
- Assert.Equal (19, tv.SelectionStartColumn);
- Assert.Equal (0, tv.SelectionStartRow);
- tv.SelectionStartColumn = 0;
- Assert.True (tv.NewKeyDownEvent (Key.C.WithCtrl));
-
- Assert.Equal (
- $"is is the first lin{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first",
- tv.Text
- );
- Assert.Equal (new (19, 0), tv.CursorPosition);
- Assert.Equal (19, tv.SelectedLength);
- Assert.Equal ("is is the first lin", tv.SelectedText);
- Assert.True (tv.IsSelecting);
- Assert.Equal (0, tv.SelectionStartColumn);
- Assert.Equal (0, tv.SelectionStartRow);
- Assert.True (tv.NewKeyDownEvent (Key.C.WithCtrl));
-
- Assert.Equal (
- $"is is the first lin{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first",
- tv.Text
- );
- Assert.Equal (new (19, 0), tv.CursorPosition);
- Assert.Equal (19, tv.SelectedLength);
- Assert.Equal ("is is the first lin", tv.SelectedText);
- Assert.True (tv.IsSelecting);
- Assert.Equal (0, tv.SelectionStartColumn);
- Assert.Equal (0, tv.SelectionStartRow);
- Assert.True (tv.NewKeyDownEvent (Key.X.WithCtrl));
-
- Assert.Equal (
- $"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first",
- tv.Text
- );
- Assert.Equal (Point.Empty, tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.False (tv.IsSelecting);
- Assert.Equal (0, tv.SelectionStartColumn);
- Assert.Equal (0, tv.SelectionStartRow);
- Assert.Equal ("is is the first lin", Clipboard.Contents);
- Assert.True (tv.NewKeyDownEvent (Key.W.WithCtrl));
-
- Assert.Equal (
- $"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first",
- tv.Text
- );
- Assert.Equal (Point.Empty, tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.False (tv.IsSelecting);
- Assert.Equal (0, tv.SelectionStartColumn);
- Assert.Equal (0, tv.SelectionStartRow);
- Assert.Equal ("", Clipboard.Contents);
- Assert.True (tv.NewKeyDownEvent (Key.X.WithCtrl));
-
- Assert.Equal (
- $"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first",
- tv.Text
- );
- Assert.Equal (Point.Empty, tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.False (tv.IsSelecting);
- Assert.Equal (0, tv.SelectionStartColumn);
- Assert.Equal (0, tv.SelectionStartRow);
- Assert.Equal ("", Clipboard.Contents);
- Assert.True (tv.NewKeyDownEvent (Key.End.WithCtrl));
-
- Assert.Equal (
- $"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first",
- tv.Text
- );
- Assert.Equal (new (28, 2), tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.False (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.CursorLeft.WithCtrl));
-
- Assert.Equal (
- $"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first",
- tv.Text
- );
- Assert.Equal (new (23, 2), tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.False (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.CursorLeft.WithCtrl));
-
- Assert.Equal (
- $"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first",
- tv.Text
- );
- Assert.Equal (new (22, 2), tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.False (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.CursorLeft.WithCtrl));
-
- Assert.Equal (
- $"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first",
- tv.Text
- );
- Assert.Equal (new (18, 2), tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.False (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.CursorLeft.WithShift.WithCtrl));
-
- Assert.Equal (
- $"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first",
- tv.Text
- );
- Assert.Equal (new (12, 2), tv.CursorPosition);
- Assert.Equal (6, tv.SelectedLength);
- Assert.Equal ("third ", tv.SelectedText);
- Assert.True (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.CursorLeft.WithCtrl));
-
- Assert.Equal (
- $"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first",
- tv.Text
- );
- Assert.Equal (new (8, 2), tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.False (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.CursorRight.WithCtrl));
-
- Assert.Equal (
- $"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first",
- tv.Text
- );
- Assert.Equal (new (12, 2), tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.False (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.CursorRight.WithShift.WithCtrl));
-
- Assert.Equal (
- $"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first",
- tv.Text
- );
- Assert.Equal (new (18, 2), tv.CursorPosition);
- Assert.Equal (6, tv.SelectedLength);
- Assert.Equal ("third ", tv.SelectedText);
- Assert.True (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.CursorRight.WithCtrl));
-
- Assert.Equal (
- $"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first",
- tv.Text
- );
- Assert.Equal (new (22, 2), tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.False (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.CursorRight.WithCtrl));
-
- Assert.Equal (
- $"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first",
- tv.Text
- );
- Assert.Equal (new (23, 2), tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.False (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.CursorRight.WithCtrl));
-
- Assert.Equal (
- $"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first",
- tv.Text
- );
- Assert.Equal (new (28, 2), tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.False (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.Home.WithCtrl));
-
- Assert.Equal (
- $"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first",
- tv.Text
- );
- Assert.Equal (Point.Empty, tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.False (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.Delete.WithCtrl));
- Assert.Equal ($"This is the second line.{Environment.NewLine}This is the third line.first", tv.Text);
- Assert.Equal (Point.Empty, tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.False (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.End.WithCtrl));
- Assert.Equal ($"This is the second line.{Environment.NewLine}This is the third line.first", tv.Text);
- Assert.Equal (new (28, 1), tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.False (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.Backspace.WithCtrl));
- Assert.Equal ($"This is the second line.{Environment.NewLine}This is the third line.", tv.Text);
- Assert.Equal (new (23, 1), tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.False (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.Backspace.WithCtrl));
- Assert.Equal ($"This is the second line.{Environment.NewLine}This is the third line", tv.Text);
- Assert.Equal (new (22, 1), tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.False (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.Backspace.WithCtrl));
- Assert.Equal ($"This is the second line.{Environment.NewLine}This is the third ", tv.Text);
- Assert.Equal (new (18, 1), tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.False (tv.IsSelecting);
- Assert.True (tv.AllowsReturn);
-
- tv.AllowsReturn = false;
- Assert.Equal (Point.Empty, tv.CursorPosition);
- Assert.False (tv.IsSelecting);
- Assert.False (tv.NewKeyDownEvent (Key.Enter)); // Accepted event not handled
- Assert.Equal ($"This is the second line.{Environment.NewLine}This is the third ", tv.Text);
- Assert.Equal (Point.Empty, tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.False (tv.IsSelecting);
- Assert.False (tv.AllowsReturn);
-
- tv.AllowsReturn = true;
- Assert.Equal (Point.Empty, tv.CursorPosition);
- Assert.True (tv.NewKeyDownEvent (Key.Enter));
-
- Assert.Equal (
- $"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third ",
- tv.Text
- );
- Assert.Equal (new (0, 1), tv.CursorPosition);
- Assert.Equal (0, tv.SelectedLength);
- Assert.Equal ("", tv.SelectedText);
- Assert.False (tv.IsSelecting);
- Assert.True (tv.AllowsReturn);
- Assert.True (tv.NewKeyDownEvent (Key.End.WithShift.WithCtrl));
-
- Assert.Equal (
- $"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third ",
- tv.Text
- );
- Assert.Equal (new (18, 2), tv.CursorPosition);
- Assert.Equal (42 + Environment.NewLine.Length, tv.SelectedLength);
- Assert.Equal ($"This is the second line.{Environment.NewLine}This is the third ", tv.SelectedText);
- Assert.True (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.Home.WithShift.WithCtrl));
-
- Assert.Equal (
- $"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third ",
- tv.Text
- );
- Assert.Equal (Point.Empty, tv.CursorPosition);
- Assert.Equal (Environment.NewLine.Length, tv.SelectedLength);
- Assert.Equal ($"{Environment.NewLine}", tv.SelectedText);
- Assert.True (tv.IsSelecting);
- Assert.True (tv.NewKeyDownEvent (Key.A.WithCtrl));
-
- Assert.Equal (
- $"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third ",
- tv.Text
- );
- Assert.Equal (new (18, 2), tv.CursorPosition);
- Assert.Equal (42 + Environment.NewLine.Length * 2, tv.SelectedLength);
-
- Assert.Equal (
- $"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third ",
- tv.SelectedText
- );
- Assert.True (tv.IsSelecting);
- Assert.True (tv.Used);
- Assert.True (tv.NewKeyDownEvent (Key.InsertChar));
- Assert.False (tv.Used);
- Assert.True (tv.AllowsTab);
- Assert.Equal (new (18, 2), tv.CursorPosition);
- Assert.True (tv.IsSelecting);
- tv.AllowsTab = false;
- Assert.False (tv.NewKeyDownEvent (Key.Tab));
-
- Assert.Equal (
- $"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third ",
- tv.Text
- );
- Assert.False (tv.AllowsTab);
- tv.AllowsTab = true;
- Assert.Equal (new (18, 2), tv.CursorPosition);
- Assert.True (tv.IsSelecting);
- tv.IsSelecting = false;
- Assert.True (tv.NewKeyDownEvent (Key.Tab));
-
- Assert.Equal (
- $"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third \t",
- tv.Text
- );
- Assert.False (tv.IsSelecting);
- Assert.True (tv.AllowsTab);
- tv.AllowsTab = false;
- Assert.False (tv.NewKeyDownEvent (Key.Tab.WithShift));
-
- Assert.Equal (
- $"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third \t",
- tv.Text
- );
- Assert.False (tv.IsSelecting);
- Assert.False (tv.AllowsTab);
- tv.AllowsTab = true;
- Assert.True (tv.NewKeyDownEvent (Key.Tab.WithShift));
-
- Assert.Equal (
- $"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third ",
- tv.Text
- );
- Assert.False (tv.IsSelecting);
- Assert.True (tv.AllowsTab);
- Assert.False (tv.NewKeyDownEvent (Key.F6));
- Assert.False (tv.NewKeyDownEvent (Application.NextTabGroupKey));
- Assert.False (tv.NewKeyDownEvent (Key.F6.WithShift));
- Assert.False (tv.NewKeyDownEvent (Application.PrevTabGroupKey));
-
- Assert.True (tv.NewKeyDownEvent (PopoverMenu.DefaultKey));
- Assert.True (tv.ContextMenu != null && tv.ContextMenu.Visible);
- Assert.False (tv.IsSelecting);
- top.Dispose ();
- }
-
- [Fact]
- [TextViewTestsSetupFakeApplication]
- public void Kill_Delete_WordBackward ()
- {
- _textView.Text = "This is the first line.";
- _textView.MoveEnd ();
- var iteration = 0;
- var iterationsFinished = false;
-
- while (!iterationsFinished)
- {
- _textView.NewKeyDownEvent (Key.Backspace.WithCtrl);
-
- switch (iteration)
- {
- case 0:
- Assert.Equal (22, _textView.CursorPosition.X);
- Assert.Equal (0, _textView.CursorPosition.Y);
- Assert.Equal ("This is the first line", _textView.Text);
-
- break;
- case 1:
- Assert.Equal (18, _textView.CursorPosition.X);
- Assert.Equal (0, _textView.CursorPosition.Y);
- Assert.Equal ("This is the first ", _textView.Text);
-
- break;
- case 2:
- Assert.Equal (12, _textView.CursorPosition.X);
- Assert.Equal (0, _textView.CursorPosition.Y);
- Assert.Equal ("This is the ", _textView.Text);
-
- break;
- case 3:
- Assert.Equal (8, _textView.CursorPosition.X);
- Assert.Equal (0, _textView.CursorPosition.Y);
- Assert.Equal ("This is ", _textView.Text);
-
- break;
- case 4:
- Assert.Equal (5, _textView.CursorPosition.X);
- Assert.Equal (0, _textView.CursorPosition.Y);
- Assert.Equal ("This ", _textView.Text);
-
- break;
- case 5:
- Assert.Equal (0, _textView.CursorPosition.X);
- Assert.Equal (0, _textView.CursorPosition.Y);
- Assert.Equal ("", _textView.Text);
-
- break;
- default:
- iterationsFinished = true;
-
- break;
- }
-
- iteration++;
- }
- }
-
- [Fact]
- [TextViewTestsSetupFakeApplication]
- public void Kill_Delete_WordBackward_Multiline ()
- {
- _textView.Text = "This is the first line.\nThis is the second line.";
- _textView.Width = 4;
- _textView.MoveEnd ();
- var iteration = 0;
- var iterationsFinished = false;
-
- while (!iterationsFinished)
- {
- _textView.NewKeyDownEvent (Key.Backspace.WithCtrl);
-
- switch (iteration)
- {
- case 0:
- Assert.Equal (23, _textView.CursorPosition.X);
- Assert.Equal (1, _textView.CursorPosition.Y);
-
- Assert.Equal (
- "This is the first line."
- + Environment.NewLine
- + "This is the second line",
- _textView.Text
- );
-
- break;
- case 1:
- Assert.Equal (19, _textView.CursorPosition.X);
- Assert.Equal (1, _textView.CursorPosition.Y);
-
- Assert.Equal (
- "This is the first line."
- + Environment.NewLine
- + "This is the second ",
- _textView.Text
- );
-
- break;
- case 2:
- Assert.Equal (12, _textView.CursorPosition.X);
- Assert.Equal (1, _textView.CursorPosition.Y);
+ break;
+ case 2:
+ Assert.Equal (12, _textView.CursorPosition.X);
+ Assert.Equal (1, _textView.CursorPosition.Y);
Assert.Equal (
"This is the first line."
@@ -4901,342 +4123,6 @@ public void Selection_With_Value_Less_Than_Zero_Changes_To_Zero ()
Assert.Equal ("", _textView.SelectedText);
}
- [Fact]
- [TextViewTestsSetupFakeApplication]
- public void Tab_Test_Follow_By_BackTab ()
- {
- var top = new Toplevel ();
- top.Add (_textView);
-
- Application.Iteration += OnApplicationOnIteration;
-
- Application.Run (top);
- Application.Iteration -= OnApplicationOnIteration;
- top.Dispose ();
-
- return;
-
- void OnApplicationOnIteration (object s, IterationEventArgs a)
- {
- int width = _textView.Viewport.Width - 1;
- Assert.Equal (30, width + 1);
- Assert.Equal (10, _textView.Height);
- _textView.Text = "";
- var col = 0;
- var leftCol = 0;
- int tabWidth = _textView.TabWidth;
-
- while (col < 100)
- {
- col++;
- _textView.NewKeyDownEvent (Key.Tab);
- Assert.Equal (new (col, 0), _textView.CursorPosition);
- leftCol = GetLeftCol (leftCol);
- Assert.Equal (leftCol, _textView.LeftColumn);
- }
-
- while (col > 0)
- {
- col--;
- _textView.NewKeyDownEvent (Key.Tab.WithShift);
- Assert.Equal (new (col, 0), _textView.CursorPosition);
- leftCol = GetLeftCol (leftCol);
- Assert.Equal (leftCol, _textView.LeftColumn);
- }
-
- top.Remove (_textView);
- Application.RequestStop ();
- }
- }
-
- [Fact]
- [TextViewTestsSetupFakeApplication]
- public void Tab_Test_Follow_By_BackTab_With_Text ()
- {
- var top = new Toplevel ();
- top.Add (_textView);
-
- Application.Iteration += OnApplicationOnIteration;
-
- Application.Run (top);
- Application.Iteration -= OnApplicationOnIteration;
- top.Dispose ();
-
- return;
-
- void OnApplicationOnIteration (object s, IterationEventArgs a)
- {
- int width = _textView.Viewport.Width - 1;
- Assert.Equal (30, width + 1);
- Assert.Equal (10, _textView.Height);
- var col = 0;
- var leftCol = 0;
- Assert.Equal (new (col, 0), _textView.CursorPosition);
- Assert.Equal (leftCol, _textView.LeftColumn);
-
- while (col < 100)
- {
- col++;
- _textView.NewKeyDownEvent (Key.Tab);
- Assert.Equal (new (col, 0), _textView.CursorPosition);
- leftCol = GetLeftCol (leftCol);
- Assert.Equal (leftCol, _textView.LeftColumn);
- }
-
- while (col > 0)
- {
- col--;
- _textView.NewKeyDownEvent (Key.Tab.WithShift);
- Assert.Equal (new (col, 0), _textView.CursorPosition);
- leftCol = GetLeftCol (leftCol);
- Assert.Equal (leftCol, _textView.LeftColumn);
- }
-
- top.Remove (_textView);
- Application.RequestStop ();
- }
- }
-
- [Fact]
- [TextViewTestsSetupFakeApplication]
- public void Tab_Test_Follow_By_CursorLeft_And_Then_Follow_By_CursorRight ()
- {
- var top = new Toplevel ();
- top.Add (_textView);
-
- Application.Iteration += OnApplicationOnIteration;
-
- Application.Run (top);
- Application.Iteration -= OnApplicationOnIteration;
- top.Dispose ();
-
- return;
-
- void OnApplicationOnIteration (object s, IterationEventArgs a)
- {
- int width = _textView.Viewport.Width - 1;
- Assert.Equal (30, width + 1);
- Assert.Equal (10, _textView.Height);
- _textView.Text = "";
- var col = 0;
- var leftCol = 0;
- int tabWidth = _textView.TabWidth;
-
- while (col < 100)
- {
- col++;
- _textView.NewKeyDownEvent (Key.Tab);
- Assert.Equal (new (col, 0), _textView.CursorPosition);
- leftCol = GetLeftCol (leftCol);
- Assert.Equal (leftCol, _textView.LeftColumn);
- }
-
- while (col > 0)
- {
- col--;
- _textView.NewKeyDownEvent (Key.CursorLeft);
- Assert.Equal (new (col, 0), _textView.CursorPosition);
- leftCol = GetLeftCol (leftCol);
- Assert.Equal (leftCol, _textView.LeftColumn);
- }
-
- while (col < 100)
- {
- col++;
- _textView.NewKeyDownEvent (Key.CursorRight);
- Assert.Equal (new (col, 0), _textView.CursorPosition);
- leftCol = GetLeftCol (leftCol);
- Assert.Equal (leftCol, _textView.LeftColumn);
- }
-
- top.Remove (_textView);
- Application.RequestStop ();
- }
- }
-
- [Fact]
- [TextViewTestsSetupFakeApplication]
- public void Tab_Test_Follow_By_CursorLeft_And_Then_Follow_By_CursorRight_With_Text ()
- {
- var top = new Toplevel ();
- top.Add (_textView);
-
- Application.Iteration += OnApplicationOnIteration;
-
- Application.Run (top);
- Application.Iteration -= OnApplicationOnIteration;
- top.Dispose ();
-
- return;
-
- void OnApplicationOnIteration (object s, IterationEventArgs a)
- {
- int width = _textView.Viewport.Width - 1;
- Assert.Equal (30, width + 1);
- Assert.Equal (10, _textView.Height);
- Assert.Equal ("TAB to jump between text fields.", _textView.Text);
- var col = 0;
- var leftCol = 0;
- int tabWidth = _textView.TabWidth;
-
- while (col < 100)
- {
- col++;
- _textView.NewKeyDownEvent (Key.Tab);
- Assert.Equal (new (col, 0), _textView.CursorPosition);
- leftCol = GetLeftCol (leftCol);
- Assert.Equal (leftCol, _textView.LeftColumn);
- }
-
- Assert.Equal (132, _textView.Text.Length);
-
- while (col > 0)
- {
- col--;
- _textView.NewKeyDownEvent (Key.CursorLeft);
- Assert.Equal (new (col, 0), _textView.CursorPosition);
- leftCol = GetLeftCol (leftCol);
- Assert.Equal (leftCol, _textView.LeftColumn);
- }
-
- while (col < 100)
- {
- col++;
- _textView.NewKeyDownEvent (Key.CursorRight);
- Assert.Equal (new (col, 0), _textView.CursorPosition);
- leftCol = GetLeftCol (leftCol);
- Assert.Equal (leftCol, _textView.LeftColumn);
- }
-
- top.Remove (_textView);
- Application.RequestStop ();
- }
- }
-
- [Fact]
- [TextViewTestsSetupFakeApplication]
- public void Tab_Test_Follow_By_Home_And_Then_Follow_By_End_And_Then_Follow_By_BackTab_With_Text ()
- {
- var top = new Toplevel ();
- top.Add (_textView);
-
- Application.Iteration += OnApplicationOnIteration;
-
- Application.Run (top);
- Application.Iteration -= OnApplicationOnIteration;
- top.Dispose ();
-
- return;
-
- void OnApplicationOnIteration (object s, IterationEventArgs a)
- {
- int width = _textView.Viewport.Width - 1;
- Assert.Equal (30, width + 1);
- Assert.Equal (10, _textView.Height);
- var col = 0;
- var leftCol = 0;
- Assert.Equal (new (col, 0), _textView.CursorPosition);
- Assert.Equal (leftCol, _textView.LeftColumn);
- Assert.Equal ("TAB to jump between text fields.", _textView.Text);
- Assert.Equal (32, _textView.Text.Length);
-
- while (col < 100)
- {
- col++;
- _textView.NewKeyDownEvent (Key.Tab);
- Assert.Equal (new (col, 0), _textView.CursorPosition);
- leftCol = GetLeftCol (leftCol);
- Assert.Equal (leftCol, _textView.LeftColumn);
- }
-
- _textView.NewKeyDownEvent (Key.Home);
- col = 0;
- Assert.Equal (new (col, 0), _textView.CursorPosition);
- leftCol = 0;
- Assert.Equal (leftCol, _textView.LeftColumn);
-
- _textView.NewKeyDownEvent (Key.End);
- col = _textView.Text.Length;
- Assert.Equal (132, _textView.Text.Length);
- Assert.Equal (new (col, 0), _textView.CursorPosition);
- leftCol = GetLeftCol (leftCol);
- Assert.Equal (leftCol, _textView.LeftColumn);
- string txt = _textView.Text;
-
- while (col - 1 > 0 && txt [col - 1] != '\t')
- {
- col--;
- }
-
- _textView.CursorPosition = new (col, 0);
- leftCol = GetLeftCol (leftCol);
-
- while (col > 0)
- {
- col--;
- _textView.NewKeyDownEvent (Key.Tab.WithShift);
- Assert.Equal (new (col, 0), _textView.CursorPosition);
- leftCol = GetLeftCol (leftCol);
- Assert.Equal (leftCol, _textView.LeftColumn);
- }
-
- Assert.Equal ("TAB to jump between text fields.", _textView.Text);
- Assert.Equal (32, _textView.Text.Length);
-
- top.Remove (_textView);
- Application.RequestStop ();
- }
- }
-
- [Fact]
- [TextViewTestsSetupFakeApplication]
- public void TabWidth_Setting_To_Zero_Keeps_AllowsTab ()
- {
- var top = new Toplevel ();
- top.Add (_textView);
- Application.Begin (top);
-
- Assert.Equal (4, _textView.TabWidth);
- Assert.True (_textView.AllowsTab);
- Assert.True (_textView.AllowsReturn);
- Assert.True (_textView.Multiline);
- _textView.TabWidth = -1;
- Assert.Equal (0, _textView.TabWidth);
- Assert.True (_textView.AllowsTab);
- Assert.True (_textView.AllowsReturn);
- Assert.True (_textView.Multiline);
- _textView.NewKeyDownEvent (Key.Tab);
- Assert.Equal ("\tTAB to jump between text fields.", _textView.Text);
- SetupFakeApplicationAttribute.RunIteration ();
-
- DriverAssert.AssertDriverContentsWithFrameAre (
- @"
-TAB to jump between text field",
- _output
- );
-
- _textView.TabWidth = 4;
- SetupFakeApplicationAttribute.RunIteration ();
-
- DriverAssert.AssertDriverContentsWithFrameAre (
- @"
- TAB to jump between text f",
- _output
- );
-
- _textView.NewKeyDownEvent (Key.Tab.WithShift);
- Assert.Equal ("TAB to jump between text fields.", _textView.Text);
- Assert.True (_textView.NeedsDraw);
- SetupFakeApplicationAttribute.RunIteration ();
-
- DriverAssert.AssertDriverContentsWithFrameAre (
- @"
-TAB to jump between text field",
- _output
- );
- top.Dispose ();
- }
-
[Fact]
[TextViewTestsSetupFakeApplication]
public void TextChanged_Event ()
@@ -5440,144 +4326,6 @@ public void TextView_SpaceHandling ()
tv.NewMouseEvent (ev);
Assert.Equal (1, tv.SelectedLength);
}
-
- [Fact]
- [SetupFakeApplication]
- public void UnwrappedCursorPosition_Event ()
- {
- var cp = Point.Empty;
-
- var tv = new TextView
- {
- Width = Dim.Fill (), Height = Dim.Fill (), Text = "This is the first line.\nThis is the second line.\n"
- };
- tv.UnwrappedCursorPosition += (s, e) => { cp = e; };
- var top = new Toplevel ();
- top.Add (tv);
- Application.Begin (top);
- SetupFakeApplicationAttribute.RunIteration ();
-
- Assert.False (tv.WordWrap);
- Assert.Equal (Point.Empty, tv.CursorPosition);
- Assert.Equal (Point.Empty, cp);
-
- DriverAssert.AssertDriverContentsWithFrameAre (
- @"
-This is the first line.
-This is the second line.
-",
- _output
- );
-
- tv.WordWrap = true;
- tv.CursorPosition = new (12, 0);
- tv.Draw ();
- Assert.Equal (new (12, 0), tv.CursorPosition);
- Assert.Equal (new (12, 0), cp);
-
- DriverAssert.AssertDriverContentsWithFrameAre (
- @"
-This is the first line.
-This is the second line.
-",
- _output
- );
-
- Application.Driver!.SetScreenSize (6, 25);
- tv.SetRelativeLayout (Application.Screen.Size);
- tv.Draw ();
- Assert.Equal (new (4, 2), tv.CursorPosition);
- Assert.Equal (new (12, 0), cp);
-
- DriverAssert.AssertDriverContentsWithFrameAre (
- @"
-This
-is
-the
-first
-
-line.
-This
-is
-the
-secon
-d
-line.
-",
- _output
- );
-
- Assert.True (tv.NewKeyDownEvent (Key.CursorRight));
- tv.Draw ();
- Assert.Equal (new (0, 3), tv.CursorPosition);
- Assert.Equal (new (12, 0), cp);
-
- DriverAssert.AssertDriverContentsWithFrameAre (
- @"
-This
-is
-the
-first
-
-line.
-This
-is
-the
-secon
-d
-line.
-",
- _output
- );
-
- Assert.True (tv.NewKeyDownEvent (Key.CursorRight));
- tv.Draw ();
- Assert.Equal (new (1, 3), tv.CursorPosition);
- Assert.Equal (new (13, 0), cp);
-
- DriverAssert.AssertDriverContentsWithFrameAre (
- @"
-This
-is
-the
-first
-
-line.
-This
-is
-the
-secon
-d
-line.
-",
- _output
- );
-
- Assert.True (tv.NewMouseEvent (new () { Position = new (0, 3), Flags = MouseFlags.Button1Pressed }));
- tv.Draw ();
- Assert.Equal (new (0, 3), tv.CursorPosition);
- Assert.Equal (new (12, 0), cp);
-
- DriverAssert.AssertDriverContentsWithFrameAre (
- @"
-This
-is
-the
-first
-
-line.
-This
-is
-the
-secon
-d
-line.
-",
- _output
- );
- top.Dispose ();
- }
-
[Fact]
[TextViewTestsSetupFakeApplication]
public void Used_Is_False ()
diff --git a/Tests/UnitTestsParallelizable/Views/TextViewNavigationTests.cs b/Tests/UnitTestsParallelizable/Views/TextViewNavigationTests.cs
new file mode 100644
index 0000000000..fd9f9a7b89
--- /dev/null
+++ b/Tests/UnitTestsParallelizable/Views/TextViewNavigationTests.cs
@@ -0,0 +1,475 @@
+using UnitTests;
+
+namespace UnitTests_Parallelizable.ViewsTests;
+
+///
+/// Tests for TextView navigation, tabs, and cursor positioning.
+/// These replace the old non-parallelizable tests that had hard-coded viewport dimensions.
+///
+public class TextViewNavigationTests : FakeDriverBase
+{
+ [Fact]
+ public void Tab_And_BackTab_Navigation_Without_Text ()
+ {
+ var textView = new TextView
+ {
+ Width = 30,
+ Height = 10,
+ Text = ""
+ };
+ textView.BeginInit ();
+ textView.EndInit ();
+
+ // Add 100 tabs
+ for (var i = 0; i < 100; i++)
+ {
+ textView.Text += "\t";
+ }
+
+ // Move to end
+ textView.MoveEnd ();
+ Assert.Equal (new Point (100, 0), textView.CursorPosition);
+
+ // Test BackTab (Shift+Tab) navigation backwards
+ for (var i = 99; i >= 0; i--)
+ {
+ Assert.True (textView.NewKeyDownEvent (Key.Tab.WithShift));
+ Assert.Equal (new Point (i, 0), textView.CursorPosition);
+ }
+
+ // Test Tab navigation forwards
+ for (var i = 1; i <= 100; i++)
+ {
+ Assert.True (textView.NewKeyDownEvent (Key.Tab));
+ Assert.Equal (new Point (i, 0), textView.CursorPosition);
+ }
+ }
+
+ [Fact]
+ public void Tab_And_BackTab_Navigation_With_Text ()
+ {
+ var textView = new TextView
+ {
+ Width = 30,
+ Height = 10,
+ Text = "TAB to jump between text fields."
+ };
+ textView.BeginInit ();
+ textView.EndInit ();
+
+ Assert.Equal (new Point (0, 0), textView.CursorPosition);
+
+ // Navigate forward with Tab
+ for (var i = 1; i <= 100; i++)
+ {
+ Assert.True (textView.NewKeyDownEvent (Key.Tab));
+ Assert.Equal (new Point (i, 0), textView.CursorPosition);
+ }
+
+ // Navigate backward with BackTab
+ for (var i = 99; i >= 0; i--)
+ {
+ Assert.True (textView.NewKeyDownEvent (Key.Tab.WithShift));
+ Assert.Equal (new Point (i, 0), textView.CursorPosition);
+ }
+ }
+
+ [Fact]
+ public void Tab_With_CursorLeft_And_CursorRight_Without_Text ()
+ {
+ var textView = new TextView
+ {
+ Width = 30,
+ Height = 10,
+ Text = ""
+ };
+ textView.BeginInit ();
+ textView.EndInit ();
+
+ // Navigate forward with Tab
+ for (var i = 1; i <= 100; i++)
+ {
+ Assert.True (textView.NewKeyDownEvent (Key.Tab));
+ Assert.Equal (new Point (i, 0), textView.CursorPosition);
+ }
+
+ // Navigate backward with CursorLeft
+ for (var i = 99; i >= 0; i--)
+ {
+ Assert.True (textView.NewKeyDownEvent (Key.CursorLeft));
+ Assert.Equal (new Point (i, 0), textView.CursorPosition);
+ }
+
+ // Navigate forward with CursorRight
+ for (var i = 1; i <= 100; i++)
+ {
+ Assert.True (textView.NewKeyDownEvent (Key.CursorRight));
+ Assert.Equal (new Point (i, 0), textView.CursorPosition);
+ }
+ }
+
+ [Fact]
+ public void Tab_With_CursorLeft_And_CursorRight_With_Text ()
+ {
+ var textView = new TextView
+ {
+ Width = 30,
+ Height = 10,
+ Text = "TAB to jump between text fields."
+ };
+ textView.BeginInit ();
+ textView.EndInit ();
+
+ Assert.Equal (32, textView.Text.Length);
+ Assert.Equal (new Point (0, 0), textView.CursorPosition);
+
+ // Navigate forward with Tab
+ for (var i = 1; i <= 100; i++)
+ {
+ Assert.True (textView.NewKeyDownEvent (Key.Tab));
+ Assert.Equal (new Point (i, 0), textView.CursorPosition);
+ }
+
+ // Navigate backward with CursorLeft
+ for (var i = 99; i >= 0; i--)
+ {
+ Assert.True (textView.NewKeyDownEvent (Key.CursorLeft));
+ Assert.Equal (new Point (i, 0), textView.CursorPosition);
+ }
+
+ // Navigate forward with CursorRight
+ for (var i = 1; i <= 100; i++)
+ {
+ Assert.True (textView.NewKeyDownEvent (Key.CursorRight));
+ Assert.Equal (new Point (i, 0), textView.CursorPosition);
+ }
+ }
+
+ [Fact]
+ public void Tab_With_Home_End_And_BackTab ()
+ {
+ var textView = new TextView
+ {
+ Width = 30,
+ Height = 10,
+ Text = "TAB to jump between text fields."
+ };
+ textView.BeginInit ();
+ textView.EndInit ();
+
+ Assert.Equal (32, textView.Text.Length);
+ Assert.Equal (new Point (0, 0), textView.CursorPosition);
+
+ // Navigate forward with Tab to column 100
+ for (var i = 1; i <= 100; i++)
+ {
+ Assert.True (textView.NewKeyDownEvent (Key.Tab));
+ Assert.Equal (new Point (i, 0), textView.CursorPosition);
+ }
+
+ // Test Length increased due to tabs
+ Assert.Equal (132, textView.Text.Length);
+
+ // Press Home to go to beginning
+ Assert.True (textView.NewKeyDownEvent (Key.Home));
+ Assert.Equal (new Point (0, 0), textView.CursorPosition);
+
+ // Press End to go to end
+ Assert.True (textView.NewKeyDownEvent (Key.End));
+ Assert.Equal (132, textView.Text.Length);
+ Assert.Equal (new Point (132, 0), textView.CursorPosition);
+
+ // Find the position just before the last tab
+ string txt = textView.Text;
+ var col = txt.Length;
+
+ // Find the last tab position
+ while (col > 1 && txt [col - 1] != '\t')
+ {
+ col--;
+
+ // Safety check to prevent infinite loop
+ if (col == 0)
+ {
+ break;
+ }
+ }
+
+ // Set cursor to that position
+ textView.CursorPosition = new Point (col, 0);
+
+ // Navigate backward with BackTab (removes tabs, going back to original text)
+ while (col > 0)
+ {
+ col--;
+ Assert.True (textView.NewKeyDownEvent (Key.Tab.WithShift));
+ Assert.Equal (new Point (col, 0), textView.CursorPosition);
+ }
+
+ // Should be back at the original text
+ Assert.Equal ("TAB to jump between text fields.", textView.Text);
+ Assert.Equal (32, textView.Text.Length);
+ }
+
+ [Fact]
+ public void BackTab_Then_Tab_Navigation ()
+ {
+ var textView = new TextView
+ {
+ Width = 30,
+ Height = 10,
+ Text = ""
+ };
+ textView.BeginInit ();
+ textView.EndInit ();
+
+ // Add 100 tabs at end
+ for (var i = 0; i < 100; i++)
+ {
+ textView.Text += "\t";
+ }
+
+ textView.MoveEnd ();
+ Assert.Equal (new Point (100, 0), textView.CursorPosition);
+
+ // Navigate backward with BackTab
+ for (var i = 99; i >= 0; i--)
+ {
+ Assert.True (textView.NewKeyDownEvent (Key.Tab.WithShift));
+ Assert.Equal (new Point (i, 0), textView.CursorPosition);
+ }
+
+ // Navigate forward with Tab
+ for (var i = 1; i <= 100; i++)
+ {
+ Assert.True (textView.NewKeyDownEvent (Key.Tab));
+ Assert.Equal (new Point (i, 0), textView.CursorPosition);
+ }
+ }
+
+ [Fact]
+ public void TabWidth_Setting_To_Zero_Keeps_AllowsTab ()
+ {
+ var textView = new TextView
+ {
+ Width = 30,
+ Height = 10,
+ Text = "TAB to jump between text fields."
+ };
+ textView.BeginInit ();
+ textView.EndInit ();
+
+ // Verify initial state
+ Assert.Equal (4, textView.TabWidth);
+ Assert.True (textView.AllowsTab);
+ Assert.True (textView.AllowsReturn);
+ Assert.True (textView.Multiline);
+
+ // Set TabWidth to -1 (should clamp to 0)
+ textView.TabWidth = -1;
+ Assert.Equal (0, textView.TabWidth);
+ Assert.True (textView.AllowsTab);
+ Assert.True (textView.AllowsReturn);
+ Assert.True (textView.Multiline);
+
+ // Insert a tab
+ Assert.True (textView.NewKeyDownEvent (Key.Tab));
+ Assert.Equal ("\tTAB to jump between text fields.", textView.Text);
+
+ // Change TabWidth back to 4
+ textView.TabWidth = 4;
+ Assert.Equal (4, textView.TabWidth);
+
+ // Remove the tab with BackTab
+ Assert.True (textView.NewKeyDownEvent (Key.Tab.WithShift));
+ Assert.Equal ("TAB to jump between text fields.", textView.Text);
+ }
+
+ [Fact]
+ public void KeyBindings_Command_Navigation ()
+ {
+ var text = "This is the first line.\nThis is the second line.\nThis is the third line.";
+ var textView = new TextView
+ {
+ Width = 10,
+ Height = 2,
+ Text = text
+ };
+ textView.BeginInit ();
+ textView.EndInit ();
+
+ Assert.Equal (
+ $"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.",
+ textView.Text
+ );
+ Assert.Equal (3, textView.Lines);
+ Assert.Equal (Point.Empty, textView.CursorPosition);
+ Assert.False (textView.ReadOnly);
+ Assert.True (textView.CanFocus);
+ Assert.False (textView.IsSelecting);
+
+ // Test that CursorLeft doesn't move when at beginning
+ textView.CanFocus = false;
+ Assert.True (textView.NewKeyDownEvent (Key.CursorLeft));
+ Assert.False (textView.IsSelecting);
+
+ textView.CanFocus = true;
+ Assert.False (textView.NewKeyDownEvent (Key.CursorLeft));
+ Assert.False (textView.IsSelecting);
+
+ // Move right
+ Assert.True (textView.NewKeyDownEvent (Key.CursorRight));
+ Assert.Equal (new Point (1, 0), textView.CursorPosition);
+ Assert.False (textView.IsSelecting);
+
+ // Move to end of document
+ Assert.True (textView.NewKeyDownEvent (Key.End.WithCtrl));
+ Assert.Equal (2, textView.CurrentRow);
+ Assert.Equal (23, textView.CurrentColumn);
+ Assert.Equal (textView.CurrentColumn, textView.GetCurrentLine ().Count);
+ Assert.Equal (new Point (23, 2), textView.CursorPosition);
+ Assert.False (textView.IsSelecting);
+
+ // Try to move right (should fail, at end)
+ Assert.False (textView.NewKeyDownEvent (Key.CursorRight));
+ Assert.False (textView.IsSelecting);
+
+ // Type a character
+ Assert.True (textView.NewKeyDownEvent (Key.F.WithShift));
+ Assert.Equal (
+ $"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.F",
+ textView.Text
+ );
+ Assert.Equal (new Point (24, 2), textView.CursorPosition);
+ Assert.False (textView.IsSelecting);
+
+ // Undo
+ Assert.True (textView.NewKeyDownEvent (Key.Z.WithCtrl));
+ Assert.Equal (
+ $"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.",
+ textView.Text
+ );
+ Assert.Equal (new Point (23, 2), textView.CursorPosition);
+ Assert.False (textView.IsSelecting);
+
+ // Redo
+ Assert.True (textView.NewKeyDownEvent (Key.R.WithCtrl));
+ Assert.Equal (
+ $"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.F",
+ textView.Text
+ );
+ Assert.Equal (new Point (24, 2), textView.CursorPosition);
+ Assert.False (textView.IsSelecting);
+ }
+
+ [Fact]
+ public void UnwrappedCursorPosition_Event_Fires_Correctly ()
+ {
+ Point unwrappedPosition = Point.Empty;
+
+ var textView = new TextView
+ {
+ Width = 25,
+ Height = 25,
+ Text = "This is the first line.\nThis is the second line.\n"
+ };
+
+ textView.UnwrappedCursorPosition += (s, e) => { unwrappedPosition = e; };
+
+ textView.BeginInit ();
+ textView.EndInit ();
+
+ // Initially no word wrap
+ Assert.False (textView.WordWrap);
+ Assert.Equal (Point.Empty, textView.CursorPosition);
+ Assert.Equal (Point.Empty, unwrappedPosition);
+
+ // Enable word wrap and move cursor
+ textView.WordWrap = true;
+ textView.CursorPosition = new Point (12, 0);
+ Assert.Equal (new Point (12, 0), textView.CursorPosition);
+ Assert.Equal (new Point (12, 0), unwrappedPosition);
+
+ // Move right and verify unwrapped position updates
+ var currentUnwrapped = unwrappedPosition;
+ Assert.True (textView.NewKeyDownEvent (Key.CursorRight));
+ // The unwrapped position should have updated
+ Assert.True (unwrappedPosition.X >= currentUnwrapped.X);
+
+ // Move several more times to verify tracking continues to work
+ for (int i = 0; i < 5; i++)
+ {
+ Assert.True (textView.NewKeyDownEvent (Key.CursorRight));
+ }
+
+ // Unwrapped position should track the actual position in the text
+ Assert.True (unwrappedPosition.X > 12);
+ }
+
+ [Fact]
+ public void Horizontal_Scrolling_Adjusts_LeftColumn ()
+ {
+ var textView = new TextView
+ {
+ Width = 20,
+ Height = 5,
+ Text = "This is a very long line that will require horizontal scrolling to see all of it",
+ WordWrap = false
+ };
+ textView.BeginInit ();
+ textView.EndInit ();
+
+ // Initially at the start
+ Assert.Equal (0, textView.LeftColumn);
+ Assert.Equal (new Point (0, 0), textView.CursorPosition);
+
+ // Move to the end of the line
+ textView.MoveEnd ();
+
+ // LeftColumn should have adjusted to show the cursor
+ Assert.True (textView.LeftColumn > 0);
+ Assert.Equal (textView.Text.Length, textView.CurrentColumn);
+
+ // Move back to the start
+ textView.MoveHome ();
+
+ // LeftColumn should be back to 0
+ Assert.Equal (0, textView.LeftColumn);
+ Assert.Equal (0, textView.CurrentColumn);
+ }
+
+ [Fact]
+ public void Vertical_Scrolling_Adjusts_TopRow ()
+ {
+ var lines = string.Join ("\n", Enumerable.Range (1, 100).Select (i => $"Line {i}"));
+ var textView = new TextView
+ {
+ Width = 20,
+ Height = 5,
+ Text = lines
+ };
+ textView.BeginInit ();
+ textView.EndInit ();
+
+ // Initially at the top
+ Assert.Equal (0, textView.TopRow);
+ Assert.Equal (new Point (0, 0), textView.CursorPosition);
+
+ // Move down many lines
+ for (var i = 0; i < 50; i++)
+ {
+ textView.NewKeyDownEvent (Key.CursorDown);
+ }
+
+ // TopRow should have adjusted to show the cursor
+ Assert.True (textView.TopRow > 0);
+ Assert.Equal (50, textView.CurrentRow);
+
+ // Move back to the top
+ textView.NewKeyDownEvent (Key.Home.WithCtrl);
+
+ // TopRow should be back to 0
+ Assert.Equal (0, textView.TopRow);
+ Assert.Equal (0, textView.CurrentRow);
+ }
+}
diff --git a/Tests/UnitTestsParallelizable/Views/TextViewScrollingTests.cs b/Tests/UnitTestsParallelizable/Views/TextViewScrollingTests.cs
new file mode 100644
index 0000000000..9632700855
--- /dev/null
+++ b/Tests/UnitTestsParallelizable/Views/TextViewScrollingTests.cs
@@ -0,0 +1,173 @@
+using UnitTests;
+
+namespace UnitTests_Parallelizable.ViewsTests;
+
+///
+/// Tests for TextView's modern View-based scrolling infrastructure integration.
+///
+public class TextViewScrollingTests : FakeDriverBase
+{
+ [Fact]
+ public void TextView_Uses_SetContentSize_For_Scrolling ()
+ {
+ IDriver driver = CreateFakeDriver ();
+
+ var textView = new TextView
+ {
+ Width = 10,
+ Height = 5,
+ Text = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6\nLine 7",
+ Driver = driver
+ };
+ textView.BeginInit ();
+ textView.EndInit ();
+
+ // Content size should reflect the number of lines
+ Size contentSize = textView.GetContentSize ();
+ Assert.Equal (7, contentSize.Height); // 7 lines of text
+ Assert.True (contentSize.Width >= 6); // At least as wide as "Line 1"
+ }
+
+ [Fact]
+ public void VerticalScrollBar_AutoShow_Enabled_By_Default ()
+ {
+ IDriver driver = CreateFakeDriver ();
+
+ var textView = new TextView
+ {
+ Width = 10,
+ Height = 3,
+ Driver = driver
+ };
+ textView.BeginInit ();
+ textView.EndInit ();
+
+ // VerticalScrollBar should have AutoShow enabled
+ Assert.True (textView.VerticalScrollBar.AutoShow);
+ }
+
+ [Fact]
+ public void HorizontalScrollBar_AutoShow_Tracks_WordWrap ()
+ {
+ IDriver driver = CreateFakeDriver ();
+
+ var textView = new TextView
+ {
+ Width = 10,
+ Height = 3,
+ WordWrap = false,
+ Driver = driver
+ };
+ textView.BeginInit ();
+ textView.EndInit ();
+
+ // When WordWrap is false, HorizontalScrollBar AutoShow should be true
+ Assert.True (textView.HorizontalScrollBar.AutoShow);
+
+ // When WordWrap is true, HorizontalScrollBar AutoShow should be false
+ textView.WordWrap = true;
+ Assert.False (textView.HorizontalScrollBar.AutoShow);
+ }
+
+ [Fact]
+ public void TextView_Viewport_Syncs_With_Scrolling ()
+ {
+ IDriver driver = CreateFakeDriver ();
+
+ var textView = new TextView
+ {
+ Width = 20,
+ Height = 5,
+ Text = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6\nLine 7",
+ Driver = driver
+ };
+ textView.BeginInit ();
+ textView.EndInit ();
+
+ // Initially, Viewport.Y should be 0
+ Assert.Equal (0, textView.Viewport.Y);
+ Assert.Equal (0, textView.TopRow);
+
+ // Scroll down
+ textView.TopRow = 2;
+
+ // Viewport.Y should update to match
+ Assert.Equal (2, textView.Viewport.Y);
+ Assert.Equal (2, textView.TopRow);
+ }
+
+ [Fact]
+ public void TextView_ContentSize_Updates_When_Text_Changes ()
+ {
+ IDriver driver = CreateFakeDriver ();
+
+ var textView = new TextView
+ {
+ Width = 20,
+ Height = 5,
+ Text = "Short",
+ Driver = driver
+ };
+ textView.BeginInit ();
+ textView.EndInit ();
+
+ Size initialContentSize = textView.GetContentSize ();
+ Assert.Equal (1, initialContentSize.Height); // 1 line
+
+ // Add more lines
+ textView.Text = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5";
+
+ Size newContentSize = textView.GetContentSize ();
+ Assert.Equal (5, newContentSize.Height); // 5 lines
+ }
+
+ [Fact]
+ public void TextView_LeftColumn_Syncs_With_Viewport_X ()
+ {
+ IDriver driver = CreateFakeDriver ();
+
+ var textView = new TextView
+ {
+ Width = 10,
+ Height = 3,
+ Text = "This is a very long line that should require horizontal scrolling",
+ WordWrap = false,
+ Driver = driver
+ };
+ textView.BeginInit ();
+ textView.EndInit ();
+
+ // Initially at column 0
+ Assert.Equal (0, textView.Viewport.X);
+ Assert.Equal (0, textView.LeftColumn);
+
+ // Scroll horizontally
+ textView.LeftColumn = 5;
+
+ // Viewport.X should update
+ Assert.Equal (5, textView.Viewport.X);
+ Assert.Equal (5, textView.LeftColumn);
+ }
+
+ [Fact]
+ public void TextView_ScrollTo_Updates_Viewport ()
+ {
+ IDriver driver = CreateFakeDriver ();
+
+ var textView = new TextView
+ {
+ Width = 20,
+ Height = 5,
+ Text = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6\nLine 7\nLine 8\nLine 9\nLine 10",
+ Driver = driver
+ };
+ textView.BeginInit ();
+ textView.EndInit ();
+
+ // Scroll to row 3
+ textView.ScrollTo (3, isRow: true);
+
+ Assert.Equal (3, textView.TopRow);
+ Assert.Equal (3, textView.Viewport.Y);
+ }
+}
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | |