Reorganize TextView event handlers by logical functionality

Event handlers moved from TextView.EventHandlers.cs to their logical locations:

- Initialization handlers (TextView_Initialized, TextView_SuperViewChanged, TextView_ViewportChanged, TextView_LayoutComplete, Model_LinesLoaded) -> TextView.Core.cs

- History handler (HistoryText_ChangeText) -> TextView.Insert.cs

- Context menu handler (ContextMenu_KeyChanged) -> TextView.ContextMenu.cs

- Removed TextView.EventHandlers.cs

Build successful, all 163 tests pass
This commit is contained in:
Tig
2025-11-21 16:48:46 -07:00
parent 26b3fc916d
commit 4fe8df38cc
14 changed files with 3683 additions and 3570 deletions

View File

@@ -0,0 +1,151 @@
namespace Terminal.Gui.Views;
public partial class TextView
{
private void SetClipboard (string text)
{
if (text is { })
{
Clipboard.Contents = text;
}
}
/// <summary>Copy the selected text to the clipboard contents.</summary>
public void Copy ()
{
SetWrapModel ();
if (IsSelecting)
{
_copiedText = GetRegion (out _copiedCellsList);
SetClipboard (_copiedText);
_copyWithoutSelection = false;
}
else
{
List<Cell> currentLine = GetCurrentLine ();
_copiedCellsList.Add (currentLine);
_copiedText = Cell.ToString (currentLine);
SetClipboard (_copiedText);
_copyWithoutSelection = true;
}
UpdateWrapModel ();
DoNeededAction ();
}
/// <summary>Cut the selected text to the clipboard contents.</summary>
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 ();
}
/// <summary>Paste the clipboard contents into the current selected position.</summary>
public void Paste ()
{
if (_isReadOnly)
{
return;
}
SetWrapModel ();
string? contents = Clipboard.Contents;
if (_copyWithoutSelection && contents!.FirstOrDefault (x => x is '\n' or '\r') == 0)
{
List<Cell> runeList = contents is null ? [] : Cell.ToCellList (contents);
List<Cell> currentLine = GetCurrentLine ();
_historyText.Add ([new (currentLine)], CursorPosition);
List<List<Cell>> 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; }
}

View File

@@ -0,0 +1,46 @@
using System.Globalization;
namespace Terminal.Gui.Views;
/// <summary>Context menu functionality</summary>
public partial class TextView
{
private PopoverMenu CreateContextMenu ()
{
PopoverMenu menu = new (
new List<View>
{
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);
}
}

View File

@@ -0,0 +1,661 @@
using System.Globalization;
namespace Terminal.Gui.Views;
/// <summary>Core functionality - Fields, Constructor, and fundamental properties</summary>
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<List<Cell>> _copiedCellsList = [];
#endregion
#region Constructor
/// <summary>
/// Initializes a <see cref="TextView"/> on the specified area, with dimensions controlled with the X, Y, Width
/// and Height properties.
/// </summary>
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
/// <summary>
/// Configures the ScrollBars to work with the modern View scrolling system.
/// </summary>
private void ConfigureScrollBars ()
{
// Subscribe to ViewportChanged to sync internal scroll fields
ViewportChanged += TextView_ViewportChanged;
// 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_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
ConfigureScrollBars ();
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 TextView_ViewportChanged (object? sender, DrawEventArgs e)
{
// Sync internal scroll position fields with Viewport
// Only update if values actually changed to prevent infinite loops
if (_topRow != Viewport.Y)
{
_topRow = Viewport.Y;
}
if (_leftColumn != Viewport.X)
{
_leftColumn = Viewport.X;
}
}
private void TextView_LayoutComplete (object? sender, LayoutEventArgs e)
{
WrapTextModel ();
UpdateContentSize ();
Adjust ();
}
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
}

View File

@@ -0,0 +1,641 @@
namespace Terminal.Gui.Views;
public partial class TextView
{
/// <summary>Deletes all text.</summary>
public void DeleteAll ()
{
if (Lines == 0)
{
return;
}
_selectionStartColumn = 0;
_selectionStartRow = 0;
MoveBottomEndExtend ();
DeleteCharLeft ();
SetNeedsDraw ();
}
/// <summary>Deletes all the selected or a single character at left from the position of the cursor.</summary>
public void DeleteCharLeft ()
{
if (_isReadOnly)
{
return;
}
SetWrapModel ();
if (IsSelecting)
{
_historyText.Add (new () { new (GetCurrentLine ()) }, CursorPosition);
ClearSelectedRegion ();
List<Cell> currentLine = GetCurrentLine ();
_historyText.Add (
new () { new (currentLine) },
CursorPosition,
TextEditingLineStatus.Replaced
);
UpdateWrapModel ();
OnContentsChanged ();
return;
}
if (DeleteTextBackwards ())
{
UpdateWrapModel ();
OnContentsChanged ();
return;
}
UpdateWrapModel ();
DoNeededAction ();
OnContentsChanged ();
}
/// <summary>Deletes all the selected or a single character at right from the position of the cursor.</summary>
public void DeleteCharRight ()
{
if (_isReadOnly)
{
return;
}
SetWrapModel ();
if (IsSelecting)
{
_historyText.Add (new () { new (GetCurrentLine ()) }, CursorPosition);
ClearSelectedRegion ();
List<Cell> 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<Cell> 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<Cell> prevRow = _model.GetLine (prowIdx);
_historyText.Add (new () { new (prevRow) }, CursorPosition);
List<List<Cell>> 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<Cell> currentLine = GetCurrentLine ();
if (CurrentColumn == currentLine.Count)
{
if (CurrentRow + 1 == _model.Count)
{
UpdateWrapModel ();
return true;
}
_historyText.Add (new () { new (currentLine) }, CursorPosition);
List<List<Cell>> removedLines = new () { new (currentLine) };
List<Cell> 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<Cell> 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<Cell> 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<Cell> 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<List<Cell>> removedLine =
[
[..currentLine],
[]
];
_historyText.Add (
[.. removedLine],
CursorPosition,
TextEditingLineStatus.Removed
);
CurrentColumn = currentLine.Count;
}
}
else
{
int restCount = CurrentColumn;
List<Cell> 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<Cell> 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<List<Cell>> 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<Cell> 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 ();
}
}

View File

@@ -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<List<Cell>> selectedCellsOriginal = [];
List<List<Cell>> selectedCellsChanged = [];
for (int r = startRow; r <= endRow; r++)
{
List<Cell> 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<Cell> 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);
}
/// <summary>Invoked when the normal color is drawn.</summary>
public event EventHandler<CellEventArgs>? DrawNormalColor;
/// <summary>Invoked when the ready only color is drawn.</summary>
public event EventHandler<CellEventArgs>? DrawReadOnlyColor;
/// <summary>Invoked when the selection color is drawn.</summary>
public event EventHandler<CellEventArgs>? DrawSelectionColor;
/// <summary>
/// Invoked when the used color is drawn. The Used Color is used to indicate if the <see cref="Key.InsertChar"/>
/// was pressed and enabled.
/// </summary>
public event EventHandler<CellEventArgs>? DrawUsedColor;
/// <inheritdoc/>
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<Cell> 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;
}
/// <summary>
/// Sets the <see cref="View.Driver"/> to an appropriate color for rendering the given <paramref name="idxCol"/>
/// of the current <paramref name="line"/>. Override to provide custom coloring by calling
/// <see cref="View.SetAttribute"/> Defaults to <see cref="Scheme.Normal"/>.
/// </summary>
/// <param name="line">The line.</param>
/// <param name="idxCol">The col index.</param>
/// <param name="idxRow">The row index.</param>
protected virtual void OnDrawNormalColor (List<Cell> 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));
}
}
/// <summary>
/// Sets the <see cref="View.Driver"/> to an appropriate color for rendering the given <paramref name="idxCol"/>
/// of the current <paramref name="line"/>. Override to provide custom coloring by calling
/// <see cref="View.SetAttribute(Attribute)"/> Defaults to <see cref="Scheme.Focus"/>.
/// </summary>
/// <param name="line">The line.</param>
/// <param name="idxCol">The col index.</param>
/// ///
/// <param name="idxRow">The row index.</param>
protected virtual void OnDrawReadOnlyColor (List<Cell> 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);
}
}
/// <summary>
/// Sets the <see cref="View.Driver"/> to an appropriate color for rendering the given <paramref name="idxCol"/>
/// of the current <paramref name="line"/>. Override to provide custom coloring by calling
/// <see cref="View.SetAttribute(Attribute)"/> Defaults to <see cref="Scheme.Focus"/>.
/// </summary>
/// <param name="line">The line.</param>
/// <param name="idxCol">The col index.</param>
/// ///
/// <param name="idxRow">The row index.</param>
protected virtual void OnDrawSelectionColor (List<Cell> 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);
}
}
/// <summary>
/// Sets the <see cref="View.Driver"/> to an appropriate color for rendering the given <paramref name="idxCol"/>
/// of the current <paramref name="line"/>. Override to provide custom coloring by calling
/// <see cref="View.SetAttribute(Attribute)"/> Defaults to <see cref="Scheme.HotFocus"/>.
/// </summary>
/// <param name="line">The line.</param>
/// <param name="idxCol">The col index.</param>
/// ///
/// <param name="idxRow">The row index.</param>
protected virtual void OnDrawUsedColor (List<Cell> 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<Cell> 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;
}
/// <inheritdoc/>
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);
}
}

View File

@@ -0,0 +1,210 @@
namespace Terminal.Gui.Views;
/// <summary>Find and Replace functionality</summary>
public partial class TextView
{
#region Public Find/Replace Methods
/// <summary>Find the next text based on the match case with the option to replace it.</summary>
/// <param name="textToFind">The text to find.</param>
/// <param name="gaveFullTurn"><c>true</c>If all the text was forward searched.<c>false</c>otherwise.</param>
/// <param name="matchCase">The match case setting.</param>
/// <param name="matchWholeWord">The match whole word setting.</param>
/// <param name="textToReplace">The text to replace.</param>
/// <param name="replace"><c>true</c>If is replacing.<c>false</c>otherwise.</param>
/// <returns><c>true</c>If the text was found.<c>false</c>otherwise.</returns>
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);
}
/// <summary>Find the previous text based on the match case with the option to replace it.</summary>
/// <param name="textToFind">The text to find.</param>
/// <param name="gaveFullTurn"><c>true</c>If all the text was backward searched.<c>false</c>otherwise.</param>
/// <param name="matchCase">The match case setting.</param>
/// <param name="matchWholeWord">The match whole word setting.</param>
/// <param name="textToReplace">The text to replace.</param>
/// <param name="replace"><c>true</c>If the text was found.<c>false</c>otherwise.</param>
/// <returns><c>true</c>If the text was found.<c>false</c>otherwise.</returns>
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);
}
/// <summary>Reset the flag to stop continuous find.</summary>
public void FindTextChanged () { _continuousFind = false; }
/// <summary>Replaces all the text based on the match case.</summary>
/// <param name="textToFind">The text to find.</param>
/// <param name="matchCase">The match case setting.</param>
/// <param name="matchWholeWord">The match whole word setting.</param>
/// <param name="textToReplace">The text to replace.</param>
/// <returns><c>true</c>If the text was found.<c>false</c>otherwise.</returns>
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)
{
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 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<Cell> 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
}

View File

@@ -0,0 +1,309 @@
namespace Terminal.Gui.Views;
public partial class TextView
{
/// <summary>
/// Inserts the given <paramref name="toAdd"/> text at the current cursor position exactly as if the user had just
/// typed it
/// </summary>
/// <param name="toAdd">Text to add</param>
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 ();
}
}
}
private void Insert (Cell cell)
{
List<Cell> 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<List<Cell>> 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<Cell> 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<Cell>? 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<List<Cell>> 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<Cell> 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;
}
#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 ();
Adjust ();
OnContentsChanged ();
}
#endregion
}

View File

@@ -0,0 +1,599 @@
namespace Terminal.Gui.Views;
/// <summary>Navigation functionality - cursor movement and scrolling</summary>
public partial class TextView
{
#region Public Navigation Methods
/// <summary>Will scroll the <see cref="TextView"/> to the last line and position the cursor there.</summary>
public void MoveEnd ()
{
CurrentRow = _model.Count - 1;
List<Cell> line = GetCurrentLine ();
CurrentColumn = line.Count;
TrackColumn ();
DoNeededAction ();
}
/// <summary>Will scroll the <see cref="TextView"/> to the first line and position the cursor there.</summary>
public void MoveHome ()
{
CurrentRow = 0;
_topRow = 0;
CurrentColumn = 0;
_leftColumn = 0;
TrackColumn ();
DoNeededAction ();
}
/// <summary>
/// Will scroll the <see cref="TextView"/> to display the specified row at the top if <paramref name="isRow"/> is
/// true or will scroll the <see cref="TextView"/> to display the specified column at the left if
/// <paramref name="isRow"/> is false.
/// </summary>
/// <param name="idx">
/// 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
/// </param>
/// <param name="isRow">If true (default) the <paramref name="idx"/> is a row, column otherwise.</param>
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)
{
Adjust ();
}
else
{
return false;
}
DoNeededAction ();
return true;
}
private void MoveEndOfLine ()
{
List<Cell> currentLine = GetCurrentLine ();
CurrentColumn = currentLine.Count;
DoNeededAction ();
}
private bool MoveLeft ()
{
if (CurrentColumn > 0)
{
CurrentColumn--;
}
else
{
if (CurrentRow > 0)
{
CurrentRow--;
if (CurrentRow < _topRow)
{
_topRow--;
SetNeedsDraw ();
}
List<Cell> 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<Cell> 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<Cell> line = GetCurrentLine ();
if (line.Count < _columnTrack)
{
CurrentColumn = line.Count;
}
else if (_columnTrack != -1)
{
CurrentColumn = _columnTrack;
}
else if (CurrentColumn > line.Count)
{
CurrentColumn = line.Count;
}
Adjust ();
}
#endregion
}

View File

@@ -0,0 +1,399 @@
namespace Terminal.Gui.Views;
public partial class TextView
{
/// <summary>Get or sets whether the user is currently selecting text.</summary>
public bool IsSelecting { get; set; }
/// <summary>
/// Gets or sets whether the word navigation should select only the word itself without spaces around it or with the
/// spaces at right.
/// Default is <c>false</c> meaning that the spaces at right are included in the selection.
/// </summary>
public bool SelectWordOnlyOnDoubleClick { get; set; }
/// <summary>Start row position of the selected text.</summary>
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 ();
}
}
/// <summary>Start column position of the selected text.</summary>
public int SelectionStartColumn
{
get => _selectionStartColumn;
set
{
List<Cell> line = _model.GetLine (_selectionStartRow);
_selectionStartColumn = value < 0 ? 0 :
value > line.Count ? line.Count : value;
IsSelecting = true;
SetNeedsDraw ();
Adjust ();
}
}
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;
}
/// <summary>Length of the selected text.</summary>
public int SelectedLength => GetSelectedLength ();
/// <summary>
/// Gets the selected text as
/// <see>
/// <cref>List{List{Cell}}</cref>
/// </see>
/// </summary>
public List<List<Cell>> SelectedCellsList
{
get
{
GetRegion (out List<List<Cell>> selectedCellsList);
return selectedCellsList;
}
}
/// <summary>The selected text.</summary>
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<List<Cell>> 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<Cell> line = model is null ? _model.GetLine (startRow) : model.GetLine (startRow);
List<Cell> 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<Cell> 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);
}
/// <inheritdoc />
public bool EnableForDesign ()
{
Text = """
TextView provides a fully featured multi-line text editor.
It supports word wrap and history for undo.
""";
return true;
}
/// <inheritdoc/>
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<Cell> line = _model.GetLine (startRow);
_historyText.Add (new () { new (line) }, new (startCol, startRow));
List<List<Cell>> 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<Cell> 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 ();
}
/// <summary>Select all text.</summary>
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;
}
}

View File

@@ -0,0 +1,175 @@
namespace Terminal.Gui.Views;
/// <summary>Utility and helper methods for TextView</summary>
public partial class TextView
{
private void Adjust ()
{
(int width, int height) offB = OffSetBackground ();
List<Cell> 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 ();
}
private void DoNeededAction ()
{
if (!NeedsDraw && (IsSelecting || _wrapNeeded || !Used))
{
SetNeedsDraw ();
}
if (NeedsDraw)
{
Adjust ();
}
else
{
PositionCursor ();
OnUnwrappedCursorPosition ();
}
}
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);
}
/// <summary>
/// Updates the content size based on the text model dimensions.
/// </summary>
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 (but only if we have a reasonable viewport)
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));
}
private void ResetPosition ()
{
_topRow = _leftColumn = CurrentRow = CurrentColumn = 0;
StopSelecting ();
}
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 ToggleSelecting ()
{
ResetColumnTrack ();
IsSelecting = !IsSelecting;
_selectionStartColumn = CurrentColumn;
_selectionStartRow = CurrentRow;
}
}

View File

@@ -0,0 +1,125 @@
using System.Runtime.CompilerServices;
namespace Terminal.Gui.Views;
/// <summary>Word wrap functionality</summary>
public partial class TextView
{
/// <summary>Invoke the <see cref="UnwrappedCursorPosition"/> event with the unwrapped <see cref="CursorPosition"/>.</summary>
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));
}
/// <summary>Invoked with the unwrapped <see cref="CursorPosition"/>.</summary>
public event EventHandler<Point>? UnwrappedCursorPosition;
private (int Row, int Col) GetUnwrappedPosition (int line, int col)
{
if (WordWrap)
{
return new ValueTuple<int, int> (
_wrapManager!.GetModelLineFromWrappedLines (line),
_wrapManager.GetModelColFromWrappedLines (line, col)
);
}
return new ValueTuple<int, int> (line, col);
}
/// <summary>Restore from original model.</summary>
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;
}
}
/// <summary>Update the original model.</summary>
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 ();
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,9 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeEditing/GenerateMemberBody/AccessorImplementationKind/@EntryValue">BackingField</s:String>
<s:String x:Key="/Default/CodeEditing/GenerateMemberBody/DocumentationGenerationKind/@EntryValue">Inherit</s:String>
<s:String x:Key="/Default/CodeEditing/GenerateMemberBody/MethodImplementationKind/@EntryValue">ReturnDefaultValue</s:String>
<s:Boolean x:Key="/Default/CodeEditing/GenerateMemberBody/PlaceBackingFieldAboveProperty/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeEditing/GenerateMemberBody/WrapIntoRegions/@EntryValue">True</s:Boolean>
<s:Int64 x:Key="/Default/CodeEditing/NullCheckPatterns/PatternTypeNamesToPriority/=JetBrains_002EReSharper_002EFeature_002EServices_002ECSharp_002ENullChecking_002EArgumentNullExceptionThrowIfNullPattern/@EntryIndexedValue">5000</s:Int64>
<s:Int64 x:Key="/Default/CodeEditing/NullCheckPatterns/PatternTypeNamesToPriority/=JetBrains_002EReSharper_002EFeature_002EServices_002ECSharp_002ENullChecking_002EIfThenThrowPattern/@EntryIndexedValue">1000</s:Int64>
<s:Int64 x:Key="/Default/CodeEditing/NullCheckPatterns/PatternTypeNamesToPriority/=JetBrains_002EReSharper_002EFeature_002EServices_002ECSharp_002ENullChecking_002EPatternMatchingIfThenThrowPattern/@EntryIndexedValue">3000</s:Int64>
@@ -14,7 +16,7 @@
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeMissingParentheses/@EntryIndexedValue">SUGGESTION</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeModifiersOrder/@EntryIndexedValue">WARNING</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeNamespaceBody/@EntryIndexedValue">ERROR</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeNullCheckingPattern/@EntryIndexedValue">ERROR</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeNullCheckingPattern/@EntryIndexedValue">SUGGESTION</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeObjectCreationWhenTypeEvident/@EntryIndexedValue">WARNING</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeObjectCreationWhenTypeNotEvident/@EntryIndexedValue">SUGGESTION</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeRedundantParentheses/@EntryIndexedValue">WARNING</s:String>
@@ -331,6 +333,7 @@
&lt;Entry.SortBy&gt;&#xD;
&lt;Access Is="0" /&gt;&#xD;
&lt;Readonly /&gt;&#xD;
&lt;PropertyName /&gt;&#xD;
&lt;/Entry.SortBy&gt;&#xD;
&lt;/Entry&gt;&#xD;
&lt;Property DisplayName="Properties w/ Backing Field" Priority="100"&gt;&#xD;
@@ -353,14 +356,18 @@
&lt;/And&gt;&#xD;
&lt;/Entry.Match&gt;&#xD;
&lt;Entry.SortBy&gt;&#xD;
&lt;ImplementsInterface Immediate="True" /&gt;&#xD;
&lt;ImplementsInterface /&gt;&#xD;
&lt;Name /&gt;&#xD;
&lt;/Entry.SortBy&gt;&#xD;
&lt;/Entry&gt;&#xD;
&lt;Entry DisplayName="All other members"&gt;&#xD;
&lt;Entry.SortBy&gt;&#xD;
&lt;Access Is="0" /&gt;&#xD;
&lt;Name /&gt;&#xD;
&lt;Static /&gt;&#xD;
&lt;Virtual /&gt;&#xD;
&lt;Override /&gt;&#xD;
&lt;ImplementsInterface /&gt;&#xD;
&lt;Name /&gt;&#xD;
&lt;/Entry.SortBy&gt;&#xD;
&lt;/Entry&gt;&#xD;
&lt;Entry DisplayName="Nested Types"&gt;&#xD;
@@ -374,10 +381,11 @@
&lt;/Entry&gt;&#xD;
&lt;/TypePattern&gt;&#xD;
&lt;/Patterns&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/ForBuiltInTypes/@EntryValue">UseVarWhenEvident</s:String>
<s:String x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/ForBuiltInTypes/@EntryValue">UseExplicitType</s:String>
<s:String x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/ForOtherTypes/@EntryValue">UseExplicitType</s:String>
<s:String x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/ForSimpleTypes/@EntryValue">UseVarWhenEvident</s:String>
<s:String x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/ForSimpleTypes/@EntryValue">UseVar</s:String>
<s:Boolean x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/PreferSeparateDeconstructedVariablesDeclaration/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/UseRoslynLogicForEvidentTypes/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeStyle/EditorConfig/EnableClangFormatSupport/@EntryValue">False</s:Boolean>
<s:Boolean x:Key="/Default/CodeStyle/EditorConfig/EnableEditorConfigSupport/@EntryValue">False</s:Boolean>
<s:Boolean x:Key="/Default/CodeStyle/EditorConfig/ShowEditorConfigStatusBarIndicator/@EntryValue">True</s:Boolean>