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); + } +}