// // TextView.cs: multi-line text editing // // Authors: // Miguel de Icaza (miguel@gnome.org) // // // TODO: // In ReadOnly mode backspace/space behave like pageup/pagedown // Attributed text on spans // Replace insertion with Insert method // String accumulation (Control-k, control-k is not preserving the last new line, see StringToRunes // Alt-D, Alt-Backspace // API to set the cursor position // API to scroll to a particular place // keybindings to go to top/bottom // public API to insert, remove ranges // Add word forward/word backwards commands // Save buffer API // Mouse // // Desirable: // Move all the text manipulation into the TextModel using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using NStack; using Rune = System.Rune; namespace Terminal.Gui { class TextModel { List> lines = new List> (); public bool LoadFile (string file) { FilePath = file ?? throw new ArgumentNullException (nameof (file)); var stream = File.OpenRead (file); LoadStream (stream); return true; } public bool CloseFile () { if (FilePath == null) throw new ArgumentNullException (nameof (FilePath)); FilePath = null; lines = new List> (); return true; } // Turns the ustring into runes, this does not split the // contents on a newline if it is present. internal static List ToRunes (ustring str) { List runes = new List (); foreach (var x in str.ToRunes ()) { runes.Add (x); } return runes; } // Splits a string into a List that contains a List for each line public static List> StringToRunes (ustring content) { var lines = new List> (); int start = 0, i = 0; var hasCR = false; // ASCII code 13 = Carriage Return. // ASCII code 10 = Line Feed. for (; i < content.Length; i++) { if (content [i] == 13) { hasCR = true; continue; } if (content [i] == 10) { if (i - start > 0) lines.Add (ToRunes (content [start, hasCR ? i - 1 : i])); else lines.Add (ToRunes (ustring.Empty)); start = i + 1; hasCR = false; } } if (i - start >= 0) lines.Add (ToRunes (content [start, null])); return lines; } void Append (List line) { var str = ustring.Make (line.ToArray ()); lines.Add (ToRunes (str)); } public void LoadStream (Stream input) { if (input == null) throw new ArgumentNullException (nameof (input)); lines = new List> (); var buff = new BufferedStream (input); int v; var line = new List (); var wasNewLine = false; while ((v = buff.ReadByte ()) != -1) { if (v == 13) { continue; } if (v == 10) { Append (line); line.Clear (); wasNewLine = true; continue; } line.Add ((byte)v); wasNewLine = false; } if (line.Count > 0 || wasNewLine) Append (line); buff.Dispose (); } public void LoadString (ustring content) { lines = StringToRunes (content); } public override string ToString () { var sb = new StringBuilder (); for (int i = 0; i < lines.Count; i++) { sb.Append (ustring.Make (lines [i])); if ((i + 1) < lines.Count) { sb.AppendLine (); } } return sb.ToString (); } public string FilePath { get; set; } /// /// The number of text lines in the model /// public int Count => lines.Count; /// /// Returns the specified line as a List of Rune /// /// The line. /// Line number to retrieve. public List GetLine (int line) { if (lines.Count > 0) { if (line < Count) { return lines [line]; } else { return lines [Count - 1]; } } else { lines.Add (new List ()); return lines [0]; } } /// /// Adds a line to the model at the specified position. /// /// Line number where the line will be inserted. /// The line of text, as a List of Rune. public void AddLine (int pos, List runes) { lines.Insert (pos, runes); } /// /// Removes the line at the specified position /// /// Position. public void RemoveLine (int pos) { if (lines.Count > 0) { if (lines.Count == 1 && lines [0].Count == 0) { return; } lines.RemoveAt (pos); } } /// /// Returns the maximum line length of the visible lines. /// /// The first line. /// The last line. /// The tab width. public int GetMaxVisibleLine (int first, int last, int tabWidth) { int maxLength = 0; last = last < lines.Count ? last : lines.Count; for (int i = first; i < last; i++) { var line = GetLine (i); var tabSum = line.Sum (r => r == '\t' ? Math.Max (tabWidth - 1, 0) : 0); var l = line.Count + tabSum; if (l > maxLength) { maxLength = l; } } return maxLength; } internal static bool SetCol (ref int col, int width, int cols) { if (col + cols <= width) { col += cols; return true; } return false; } internal static int GetColFromX (List t, int start, int x, int tabWidth = 0) { if (x < 0) { return x; } int size = start; var pX = x + start; for (int i = start; i < t.Count; i++) { var r = t [i]; size += Rune.ColumnWidth (r); if (r == '\t') { size += tabWidth + 1; } if (i == pX || (size > pX)) { return i - start; } } return t.Count - start; } // Returns the size and length in a range of the string. internal static (int size, int length) DisplaySize (List t, int start = -1, int end = -1, bool checkNextRune = true, int tabWidth = 0) { if (t == null || t.Count == 0) { return (0, 0); } int size = 0; int len = 0; int tcount = end == -1 ? t.Count : end > t.Count ? t.Count : end; int i = start == -1 ? 0 : start; for (; i < tcount; i++) { var rune = t [i]; size += Rune.ColumnWidth (rune); len += Rune.RuneLen (rune); if (rune == '\t') { size += tabWidth + 1; len += tabWidth - 1; } if (checkNextRune && i == tcount - 1 && t.Count > tcount && IsWideRune (t [i + 1], tabWidth, out int s, out int l)) { size += s; len += l; } } bool IsWideRune (Rune r, int tWidth, out int s, out int l) { s = Rune.ColumnWidth (r); l = Rune.RuneLen (r); if (r == '\t') { s += tWidth + 1; l += tWidth - 1; } return s > 1; } return (size, len); } // Returns the left column in a range of the string. internal static int CalculateLeftColumn (List t, int start, int end, int width, int tabWidth = 0) { if (t == null || t.Count == 0) { return 0; } int size = 0; int tcount = end > t.Count - 1 ? t.Count - 1 : end; int col = 0; for (int i = tcount; i >= 0; i--) { var rune = t [i]; size += Rune.ColumnWidth (rune); if (rune == '\t') { size += tabWidth + 1; } if (size >= width) { break; } else if (end < t.Count && col > 0 && start < end && col == start) { break; } col = i; } return col; } (Point startPointToFind, Point currentPointToFind, bool found) toFind; internal (Point current, bool found) FindNextText (ustring text, out bool gaveFullTurn, bool matchCase = false, bool matchWholeWord = false) { if (text == null || lines.Count == 0) { gaveFullTurn = false; return (Point.Empty, false); } if (toFind.found) { toFind.currentPointToFind.X++; } var foundPos = GetFoundNextTextPoint (text, lines.Count, matchCase, matchWholeWord, toFind.currentPointToFind); if (!foundPos.found && toFind.currentPointToFind != toFind.startPointToFind) { foundPos = GetFoundNextTextPoint (text, toFind.startPointToFind.Y + 1, matchCase, matchWholeWord, Point.Empty); } gaveFullTurn = ApplyToFind (foundPos); return foundPos; } internal (Point current, bool found) FindPreviousText (ustring text, out bool gaveFullTurn, bool matchCase = false, bool matchWholeWord = false) { if (text == null || lines.Count == 0) { gaveFullTurn = false; return (Point.Empty, false); } if (toFind.found) { toFind.currentPointToFind.X++; } var linesCount = toFind.currentPointToFind.IsEmpty ? lines.Count - 1 : toFind.currentPointToFind.Y; var foundPos = GetFoundPreviousTextPoint (text, linesCount, matchCase, matchWholeWord, toFind.currentPointToFind); if (!foundPos.found && toFind.currentPointToFind != toFind.startPointToFind) { foundPos = GetFoundPreviousTextPoint (text, lines.Count - 1, matchCase, matchWholeWord, new Point (lines [lines.Count - 1].Count, lines.Count)); } gaveFullTurn = ApplyToFind (foundPos); return foundPos; } internal (Point current, bool found) ReplaceAllText (ustring text, bool matchCase = false, bool matchWholeWord = false, ustring textToReplace = null) { bool found = false; Point pos = Point.Empty; for (int i = 0; i < lines.Count; i++) { var x = lines [i]; var txt = GetText (x); var matchText = !matchCase ? text.ToUpper ().ToString () : text.ToString (); var col = txt.IndexOf (matchText); while (col > -1) { if (matchWholeWord && !MatchWholeWord (txt, matchText, col)) { if (col + 1 > txt.Length) { break; } col = txt.IndexOf (matchText, col + 1); continue; } if (col > -1) { if (!found) { found = true; } lines [i] = ReplaceText (x, textToReplace, matchText, col).ToRuneList (); x = lines [i]; txt = GetText (x); pos = new Point (col, i); col += (textToReplace.Length - matchText.Length); } if (col + 1 > txt.Length) { break; } col = txt.IndexOf (matchText, col + 1); } } string GetText (List x) { var txt = ustring.Make (x).ToString (); if (!matchCase) { txt = txt.ToUpper (); } return txt; } return (pos, found); } ustring ReplaceText (List source, ustring textToReplace, string matchText, int col) { var origTxt = ustring.Make (source); (int _, int len) = TextModel.DisplaySize (source, 0, col, false); (var _, var len2) = TextModel.DisplaySize (source, col, col + matchText.Length, false); (var _, var len3) = TextModel.DisplaySize (source, col + matchText.Length, origTxt.RuneCount, false); return origTxt [0, len] + textToReplace.ToString () + origTxt [len + len2, len + len2 + len3]; } bool ApplyToFind ((Point current, bool found) foundPos) { bool gaveFullTurn = false; if (foundPos.found) { toFind.currentPointToFind = foundPos.current; if (toFind.found && toFind.currentPointToFind == toFind.startPointToFind) { gaveFullTurn = true; } if (!toFind.found) { toFind.startPointToFind = toFind.currentPointToFind = foundPos.current; toFind.found = foundPos.found; } } return gaveFullTurn; } (Point current, bool found) GetFoundNextTextPoint (ustring text, int linesCount, bool matchCase, bool matchWholeWord, Point start) { for (int i = start.Y; i < linesCount; i++) { var x = lines [i]; var txt = ustring.Make (x).ToString (); if (!matchCase) { txt = txt.ToUpper (); } var matchText = !matchCase ? text.ToUpper ().ToString () : text.ToString (); var col = txt.IndexOf (matchText, Math.Min (start.X, txt.Length)); if (col > -1 && matchWholeWord && !MatchWholeWord (txt, matchText, col)) { continue; } if (col > -1 && ((i == start.Y && col >= start.X) || i > start.Y) && txt.Contains (matchText)) { return (new Point (col, i), true); } else if (col == -1 && start.X > 0) { start.X = 0; } } return (Point.Empty, false); } (Point current, bool found) GetFoundPreviousTextPoint (ustring text, int linesCount, bool matchCase, bool matchWholeWord, Point start) { for (int i = linesCount; i >= 0; i--) { var x = lines [i]; var txt = ustring.Make (x).ToString (); if (!matchCase) { txt = txt.ToUpper (); } if (start.Y != i) { start.X = Math.Max (x.Count - 1, 0); } var matchText = !matchCase ? text.ToUpper ().ToString () : text.ToString (); var col = txt.LastIndexOf (matchText, toFind.found ? start.X - 1 : start.X); if (col > -1 && matchWholeWord && !MatchWholeWord (txt, matchText, col)) { continue; } if (col > -1 && ((i <= linesCount && col <= start.X) || i < start.Y) && txt.Contains (matchText)) { return (new Point (col, i), true); } } return (Point.Empty, false); } bool MatchWholeWord (string source, string matchText, int index = 0) { if (string.IsNullOrEmpty (source) || string.IsNullOrEmpty (matchText)) { return false; } var txt = matchText.Trim (); var start = index > 0 ? index - 1 : 0; var end = index + txt.Length; if ((start == 0 || Rune.IsWhiteSpace (source [start])) && (end == source.Length || Rune.IsWhiteSpace (source [end]))) { return true; } return false; } /// /// Redefine column and line tracking. /// /// Contains the column and line. internal void ResetContinuousFind (Point point) { toFind.startPointToFind = toFind.currentPointToFind = point; toFind.found = false; } } class WordWrapManager { class WrappedLine { public int ModelLine; public int Row; public int RowIndex; public int ColWidth; } List wrappedModelLines = new List (); int frameWidth; bool isWrapModelRefreshing; public TextModel Model { get; private set; } public WordWrapManager (TextModel model) { Model = model; } public TextModel WrapModel (int width, out int nRow, out int nCol, out int nStartRow, out int nStartCol, int row = 0, int col = 0, int startRow = 0, int startCol = 0, int tabWidth = 0) { frameWidth = width; var modelRow = isWrapModelRefreshing ? row : GetModelLineFromWrappedLines (row); var modelCol = isWrapModelRefreshing ? col : GetModelColFromWrappedLines (row, col); var modelStartRow = isWrapModelRefreshing ? startRow : GetModelLineFromWrappedLines (startRow); var modelStartCol = isWrapModelRefreshing ? startCol : GetModelColFromWrappedLines (startRow, startCol); var wrappedModel = new TextModel (); int lines = 0; nRow = 0; nCol = 0; nStartRow = 0; nStartCol = 0; bool isRowAndColSetted = row == 0 && col == 0; bool isStartRowAndColSetted = startRow == 0 && startCol == 0; List wModelLines = new List (); for (int i = 0; i < Model.Count; i++) { var line = Model.GetLine (i); var wrappedLines = ToListRune ( TextFormatter.Format (ustring.Make (line), width, TextAlignment.Left, true, true, tabWidth)); int sumColWidth = 0; for (int j = 0; j < wrappedLines.Count; j++) { var wrapLine = wrappedLines [j]; if (!isRowAndColSetted && modelRow == i) { if (nCol + wrapLine.Count <= modelCol) { nCol += wrapLine.Count; nRow = lines; if (nCol == modelCol) { nCol = wrapLine.Count; isRowAndColSetted = true; } else if (j == wrappedLines.Count - 1) { nCol = wrapLine.Count - j + modelCol - nCol; isRowAndColSetted = true; } } else { var offset = nCol + wrapLine.Count - modelCol; nCol = wrapLine.Count - offset; nRow = lines; isRowAndColSetted = true; } } if (!isStartRowAndColSetted && modelStartRow == i) { if (nStartCol + wrapLine.Count <= modelStartCol) { nStartCol += wrapLine.Count; nStartRow = lines; if (nStartCol == modelStartCol) { nStartCol = wrapLine.Count; isStartRowAndColSetted = true; } else if (j == wrappedLines.Count - 1) { nStartCol = wrapLine.Count - j + modelStartCol - nStartCol; isStartRowAndColSetted = true; } } else { var offset = nStartCol + wrapLine.Count - modelStartCol; nStartCol = wrapLine.Count - offset; nStartRow = lines; isStartRowAndColSetted = true; } } wrappedModel.AddLine (lines, wrapLine); sumColWidth += wrapLine.Count; var wrappedLine = new WrappedLine () { ModelLine = i, Row = lines, RowIndex = j, ColWidth = wrapLine.Count, }; wModelLines.Add (wrappedLine); lines++; } } wrappedModelLines = wModelLines; return wrappedModel; } public List> ToListRune (List textList) { var runesList = new List> (); foreach (var text in textList) { runesList.Add (text.ToRuneList ()); } return runesList; } public int GetModelLineFromWrappedLines (int line) => wrappedModelLines.Count > 0 ? wrappedModelLines [Math.Min (line, wrappedModelLines.Count - 1)].ModelLine : 0; public int GetModelColFromWrappedLines (int line, int col) { if (wrappedModelLines?.Count == 0) { return 0; } var modelLine = GetModelLineFromWrappedLines (line); var firstLine = wrappedModelLines.IndexOf (r => r.ModelLine == modelLine); int modelCol = 0; for (int i = firstLine; i <= line; i++) { var wLine = wrappedModelLines [i]; if (i < line) { modelCol += wLine.ColWidth; } else { modelCol += col; } } return modelCol; } List GetCurrentLine (int row) => Model.GetLine (row); public void AddLine (int row, int col) { var modelRow = GetModelLineFromWrappedLines (row); var modelCol = GetModelColFromWrappedLines (row, col); var line = GetCurrentLine (modelRow); var restCount = line.Count - modelCol; var rest = line.GetRange (modelCol, restCount); line.RemoveRange (modelCol, restCount); Model.AddLine (modelRow + 1, rest); isWrapModelRefreshing = true; WrapModel (frameWidth, out _, out _, out _, out _, modelRow + 1, 0); isWrapModelRefreshing = false; } public bool Insert (int row, int col, Rune rune) { var line = GetCurrentLine (GetModelLineFromWrappedLines (row)); line.Insert (GetModelColFromWrappedLines (row, col), rune); if (line.Count > frameWidth) { return true; } else { return false; } } public bool RemoveAt (int row, int col) { var modelRow = GetModelLineFromWrappedLines (row); var line = GetCurrentLine (modelRow); var modelCol = GetModelColFromWrappedLines (row, col); if (modelCol >= line.Count) { Model.RemoveLine (modelRow); RemoveAt (row, 0); return false; } line.RemoveAt (modelCol); if (line.Count > frameWidth || (row + 1 < wrappedModelLines.Count && wrappedModelLines [row + 1].ModelLine == modelRow)) { return true; } return false; } public bool RemoveLine (int row, int col, out bool lineRemoved, bool forward = true) { lineRemoved = false; var modelRow = GetModelLineFromWrappedLines (row); var line = GetCurrentLine (modelRow); var modelCol = GetModelColFromWrappedLines (row, col); if (modelCol == 0 && line.Count == 0) { Model.RemoveLine (modelRow); return false; } else if (modelCol < line.Count) { if (forward) { line.RemoveAt (modelCol); return true; } else if (modelCol - 1 > -1) { line.RemoveAt (modelCol - 1); return true; } } lineRemoved = true; if (forward) { if (modelRow + 1 == Model.Count) { return false; } var nextLine = Model.GetLine (modelRow + 1); line.AddRange (nextLine); Model.RemoveLine (modelRow + 1); if (line.Count > frameWidth) { return true; } } else { if (modelRow == 0) { return false; } var prevLine = Model.GetLine (modelRow - 1); prevLine.AddRange (line); Model.RemoveLine (modelRow); if (prevLine.Count > frameWidth) { return true; } } return false; } public bool RemoveRange (int row, int index, int count) { var modelRow = GetModelLineFromWrappedLines (row); var line = GetCurrentLine (modelRow); var modelCol = GetModelColFromWrappedLines (row, index); try { line.RemoveRange (modelCol, count); } catch (Exception) { return false; } return true; } public void UpdateModel (TextModel model, out int nRow, out int nCol, out int nStartRow, out int nStartCol, int row, int col, int startRow, int startCol) { isWrapModelRefreshing = true; Model = model; WrapModel (frameWidth, out nRow, out nCol, out nStartRow, out nStartCol, row, col, startRow, startCol); isWrapModelRefreshing = false; } } /// /// Multi-line text editing /// /// /// /// provides a multi-line text editor. Users interact /// with it with the standard Emacs commands for movement or the arrow /// keys. /// /// /// /// 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 { TextModel model = new TextModel (); int topRow; int leftColumn; int currentRow; int currentColumn; int selectionStartColumn, selectionStartRow; bool selecting; bool wordWrap; WordWrapManager wrapManager; bool continuousFind; int bottomOffset, rightOffset; int tabWidth = 4; bool allowsTab = true; bool allowsReturn = true; bool multiline = true; /// /// Raised when the of the changes. /// public event Action TextChanged; #if false /// /// Changed event, raised when the text has clicked. /// /// /// Client code can hook up to this event, it is /// raised when the text in the entry changes. /// public Action Changed; #endif /// /// Initializes a on the specified area, with absolute position and size. /// /// /// public TextView (Rect frame) : base (frame) { Initialize (); } /// /// Initializes a on the specified area, /// with dimensions controlled with the X, Y, Width and Height properties. /// public TextView () : base () { Initialize (); } void Initialize () { CanFocus = true; Used = true; } /// /// 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; } void ResetPosition () { topRow = leftColumn = currentRow = currentColumn = 0; selecting = false; shiftSelecting = false; ResetCursorVisibility (); } /// /// Sets or gets the text in the . /// /// /// public override ustring Text { get { if (wordWrap) { return wrapManager.Model.ToString (); } else { return model.ToString (); } } set { ResetPosition (); model.LoadString (value); if (wordWrap) { wrapManager = new WordWrapManager (model); model = wrapManager.WrapModel (Frame.Width - 2, out _, out _, out _, out _); } TextChanged?.Invoke (); SetNeedsDisplay (); } } /// public override Rect Frame { get => base.Frame; set { base.Frame = value; WrapTextModel (); Adjust (); } } void WrapTextModel () { if (wordWrap && wrapManager != null) { model = wrapManager.WrapModel (Frame.Width - 2, 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; SetNeedsDisplay (); } } /// /// Gets or sets the top row. /// public int TopRow { get => topRow; set => topRow = Math.Max (Math.Min (value, Lines - 1), 0); } /// /// Gets or sets the left column. /// public int LeftColumn { get => leftColumn; set => leftColumn = Math.Max (Math.Min (value, Maxlength - 1), 0); } /// /// Gets the maximum visible length line. /// public int Maxlength => model.GetMaxVisibleLine (topRow, topRow + Frame.Height, TabWidth); /// /// Gets the number of lines. /// public int Lines => model.Count; /// /// Sets or gets the current cursor position. /// public Point CursorPosition { get => new Point (currentColumn, currentRow); set { var 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; SetNeedsDisplay (); Adjust (); } } /// /// Start column position of the selected text. /// public int SelectionStartColumn { get => selectionStartColumn; set { var line = model.GetLine (currentRow); selectionStartColumn = value < 0 ? 0 : value > line.Count ? line.Count : value; selecting = true; SetNeedsDisplay (); 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; selecting = true; SetNeedsDisplay (); Adjust (); } } /// /// The selected text. /// public ustring SelectedText { get { if (!selecting || (model.Count == 1 && model.GetLine (0).Count == 0)) { return ustring.Empty; } SetWrapModel (); var sel = GetRegion (); UpdateWrapModel (); Adjust (); return sel; } } /// /// Length of the selected text. /// public int SelectedLength => GetSelectedLength (); /// /// Get or sets the selecting. /// public bool Selecting { get => selecting; set => selecting = value; } /// /// Allows word wrap the to fit the available container width. /// public bool WordWrap { get => wordWrap; set { if (value == wordWrap) { return; } wordWrap = value; ResetPosition (); if (wordWrap) { wrapManager = new WordWrapManager (model); model = wrapManager.WrapModel (Math.Max (Frame.Width - 2, 0), out _, out _, out _, out _); } else if (!wordWrap && wrapManager != null) { model = wrapManager.Model; } SetNeedsDisplay (); } } /// /// The bottom offset needed to use a horizontal scrollbar or for another reason. /// This is only needed with the keyboard navigation. /// public int BottomOffset { get => bottomOffset; set { if (currentRow == Lines - 1 && bottomOffset > 0 && value == 0) { topRow = Math.Max (topRow - bottomOffset, 0); } bottomOffset = value; Adjust (); } } /// /// The right offset needed to use a vertical scrollbar or for another reason. /// This is only needed with the keyboard navigation. /// public int RightOffset { get => rightOffset; set { if (currentColumn == GetCurrentLine ().Count && rightOffset > 0 && value == 0) { leftColumn = Math.Max (leftColumn - rightOffset, 0); } rightOffset = value; Adjust (); } } /// /// Gets or sets a value indicating whether pressing ENTER in a /// creates a new line of text in the view or activates the default button for the toplevel. /// public bool AllowsReturn { get => allowsReturn; set { allowsReturn = value; if (allowsReturn && !multiline) { Multiline = true; } if (!allowsReturn && multiline) { Multiline = false; AllowsTab = false; } SetNeedsDisplay (); } } /// /// Gets or sets a value indicating whether pressing the TAB key in a /// types a TAB character in the view instead of moving the focus to the next view in the tab order. /// 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; } SetNeedsDisplay (); } } /// /// 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; } SetNeedsDisplay (); } } Dim savedHeight = null; /// /// 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; currentColumn = 0; currentRow = 0; savedHeight = Height; var lyout = LayoutStyle; if (LayoutStyle == LayoutStyle.Computed) { LayoutStyle = LayoutStyle.Absolute; } Height = 1; LayoutStyle = lyout; SetNeedsDisplay (); } else if (multiline && savedHeight != null) { var lyout = LayoutStyle; if (LayoutStyle == LayoutStyle.Computed) { LayoutStyle = LayoutStyle.Absolute; } Height = savedHeight; LayoutStyle = lyout; SetNeedsDisplay (); } } } int GetSelectedLength () { return SelectedText.Length; } CursorVisibility savedCursorVisibility; void SaveCursorVisibility () { if (desiredCursorVisibility != CursorVisibility.Invisible) { if (savedCursorVisibility == 0) { savedCursorVisibility = desiredCursorVisibility; } DesiredCursorVisibility = CursorVisibility.Invisible; } } void ResetCursorVisibility () { if (savedCursorVisibility == 0) { savedCursorVisibility = desiredCursorVisibility; } if (savedCursorVisibility != desiredCursorVisibility && !HasFocus) { DesiredCursorVisibility = savedCursorVisibility; savedCursorVisibility = CursorVisibility.Default; } else if (desiredCursorVisibility != CursorVisibility.Underline) { DesiredCursorVisibility = CursorVisibility.Underline; } } /// /// Loads the contents of the file into the . /// /// true, if file was loaded, false otherwise. /// Path to the file to load. public bool LoadFile (string path) { var res = model.LoadFile (path); ResetPosition (); SetNeedsDisplay (); 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 LoadStream (Stream stream) { model.LoadStream (stream); ResetPosition (); SetNeedsDisplay (); } /// /// Closes the contents of the stream into the . /// /// true, if stream was closed, false otherwise. public bool CloseFile () { var res = model.CloseFile (); ResetPosition (); SetNeedsDisplay (); return res; } /// /// Gets the current cursor row. /// public int CurrentRow => currentRow; /// /// Gets the cursor column. /// /// The cursor column. public int CurrentColumn => currentColumn; /// /// Positions the cursor on the current row and column /// public override void PositionCursor () { if (!CanFocus || !Enabled) { return; } if (selecting) { var minRow = Math.Min (Math.Max (Math.Min (selectionStartRow, currentRow) - topRow, 0), Frame.Height); var maxRow = Math.Min (Math.Max (Math.Max (selectionStartRow, currentRow) - topRow, 0), Frame.Height); SetNeedsDisplay (new Rect (0, minRow, Frame.Width, maxRow)); } var line = model.GetLine (currentRow); var col = 0; if (line.Count > 0) { for (int idx = leftColumn; idx < line.Count; idx++) { if (idx >= currentColumn) break; var cols = Rune.ColumnWidth (line [idx]); if (line [idx] == '\t') { cols += TabWidth + 1; } if (!TextModel.SetCol (ref col, Frame.Width, cols)) { col = currentColumn; break; } } } if (col >= leftColumn && currentColumn - leftColumn + RightOffset < Frame.Width && topRow <= currentRow && currentRow - topRow + BottomOffset < Frame.Height) { ResetCursorVisibility (); Move (col, currentRow - topRow); } else { SaveCursorVisibility (); } } 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, ' '); } } /// /// Sets the driver to the default color for the control where no text is being rendered. Defaults to . /// protected virtual void ColorNormal () { Driver.SetAttribute (GetNormalColor ()); } /// /// Sets the to an appropriate color for rendering the given of the /// current . Override to provide custom coloring by calling /// Defaults to . /// /// /// protected virtual void ColorNormal (List line, int idx) { Driver.SetAttribute (GetNormalColor ()); } /// /// Sets the to an appropriate color for rendering the given of the /// current . Override to provide custom coloring by calling /// Defaults to . /// /// /// protected virtual void ColorSelection (List line, int idx) { Driver.SetAttribute (ColorScheme.Focus); } /// /// Sets the to an appropriate color for rendering the given of the /// current . Override to provide custom coloring by calling /// Defaults to . /// /// /// protected virtual void ColorUsed (List line, int idx) { Driver.SetAttribute (ColorScheme.HotFocus); } bool isReadOnly = false; /// /// Gets or sets whether the is in read-only mode or not /// /// Boolean value(Default false) public bool ReadOnly { get => isReadOnly; set { isReadOnly = value; SetNeedsDisplay (); } } CursorVisibility desiredCursorVisibility = CursorVisibility.Default; /// /// Get / Set the wished cursor when the field is focused /// public CursorVisibility DesiredCursorVisibility { get => desiredCursorVisibility; set { if (desiredCursorVisibility != value && HasFocus) { Application.Driver.SetCursorVisibility (value); } desiredCursorVisibility = value; SetNeedsDisplay (); } } /// public override bool OnEnter (View view) { //TODO: Improve it by handling read only mode of the text field Application.Driver.SetCursorVisibility (DesiredCursorVisibility); return base.OnEnter (view); } // Returns an encoded region start..end (top 32 bits are the row, low32 the column) void GetEncodedRegionBounds (out long start, out long end) { long selection = ((long)(uint)selectionStartRow << 32) | (uint)selectionStartColumn; long point = ((long)(uint)currentRow << 32) | (uint)currentColumn; if (selection > point) { start = point; end = selection; } else { start = selection; end = point; } } bool PointInSelection (int col, int row) { long start, end; GetEncodedRegionBounds (out start, out end); var q = ((long)(uint)row << 32) | (uint)col; return q >= start && q <= end - 1; } // // Returns a ustring with the text in the selected // region. // ustring GetRegion () { long start, end; GetEncodedRegionBounds (out start, out end); if (start == end) { return ustring.Empty; } int startRow = (int)(start >> 32); var maxrow = ((int)(end >> 32)); int startCol = (int)(start & 0xffffffff); var endCol = (int)(end & 0xffffffff); var line = model.GetLine (startRow); if (startRow == maxrow) return StringFromRunes (line.GetRange (startCol, endCol - startCol)); ustring res = StringFromRunes (line.GetRange (startCol, line.Count - startCol)); for (int row = startRow + 1; row < maxrow; row++) { res = res + ustring.Make ((Rune)10) + StringFromRunes (model.GetLine (row)); } line = model.GetLine (maxrow); res = res + ustring.Make ((Rune)10) + StringFromRunes (line.GetRange (0, endCol)); return res; } // // Clears the contents of the selected region // void ClearRegion () { long start, end; long currentEncoded = ((long)(uint)currentRow << 32) | (uint)currentColumn; GetEncodedRegionBounds (out start, out end); int startRow = (int)(start >> 32); var maxrow = ((int)(end >> 32)); int startCol = (int)(start & 0xffffffff); var endCol = (int)(end & 0xffffffff); var line = model.GetLine (startRow); if (startRow == maxrow) { line.RemoveRange (startCol, endCol - startCol); currentColumn = startCol; if (wordWrap) { SetNeedsDisplay (); } else { SetNeedsDisplay (new Rect (0, startRow - topRow, Frame.Width, startRow - topRow + 1)); } return; } line.RemoveRange (startCol, line.Count - startCol); var line2 = model.GetLine (maxrow); line.AddRange (line2.Skip (endCol)); for (int row = startRow + 1; row <= maxrow; row++) { model.RemoveLine (startRow + 1); } if (currentEncoded == end) { currentRow -= maxrow - (startRow); } currentColumn = startCol; SetNeedsDisplay (); } /// /// 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; SetNeedsDisplay (); } /// /// 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 (ustring textToFind, out bool gaveFullTurn, bool matchCase = false, bool matchWholeWord = false, ustring textToReplace = null, bool replace = false) { if (model.Count == 0) { gaveFullTurn = false; return false; } SetWrapModel (); ResetContinuousFind (); var 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 (ustring textToFind, out bool gaveFullTurn, bool matchCase = false, bool matchWholeWord = false, ustring textToReplace = null, bool replace = false) { if (model.Count == 0) { gaveFullTurn = false; return false; } SetWrapModel (); ResetContinuousFind (); var 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 (ustring textToFind, bool matchCase = false, bool matchWholeWord = false, ustring textToReplace = null) { if (isReadOnly || model.Count == 0) { return false; } SetWrapModel (); ResetContinuousFind (); var foundPos = model.ReplaceAllText (textToFind, matchCase, matchWholeWord, textToReplace); return SetFoundText (textToFind, foundPos, textToReplace, false, true); } bool SetFoundText (ustring text, (Point current, bool found) foundPos, ustring 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.RuneCount; } else { currentColumn = selectionStartColumn + textToReplace.RuneCount; } currentRow = foundPos.current.Y; if (!isReadOnly && replace) { Adjust (); ClearSelectedRegion (); InsertText (textToReplace); StartSelecting (); selectionStartColumn = currentColumn - textToReplace.RuneCount; } else { UpdateWrapModel (); SetNeedsDisplay (); Adjust (); } continuousFind = true; return foundPos.found; } UpdateWrapModel (); continuousFind = false; return foundPos.found; } void ResetContinuousFind () { if (!continuousFind) { var col = selecting ? selectionStartColumn : currentColumn; var row = selecting ? selectionStartRow : currentRow; model.ResetContinuousFind (new Point (col, row)); } } /// /// Restore from original model. /// void SetWrapModel () { if (wordWrap) { 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. /// void UpdateWrapModel () { if (wordWrap) { wrapManager.UpdateModel (model, out int nRow, out int nCol, out int nStartRow, out int nStartCol, currentRow, currentColumn, selectionStartRow, selectionStartColumn); currentRow = nRow; currentColumn = nCol; selectionStartRow = nStartRow; selectionStartColumn = nStartCol; wrapNeeded = true; } } /// public override void Redraw (Rect bounds) { ColorNormal (); var offB = OffSetBackground (); int right = Frame.Width + offB.width + RightOffset; int bottom = Frame.Height + offB.height + BottomOffset; var row = 0; for (int idxRow = topRow; idxRow < model.Count; idxRow++) { var line = model.GetLine (idxRow); int lineRuneCount = line.Count; var col = 0; Move (0, row); for (int idxCol = leftColumn; idxCol < lineRuneCount; idxCol++) { var rune = idxCol >= lineRuneCount ? ' ' : line [idxCol]; var cols = Rune.ColumnWidth (rune); if (idxCol < line.Count && selecting && PointInSelection (idxCol, idxRow)) { ColorSelection (line, idxCol); } else if (idxCol == currentColumn && idxRow == currentRow && !selecting && !Used && HasFocus && idxCol < lineRuneCount) { ColorUsed (line, idxCol); } else { ColorNormal (line, idxCol); } if (rune == '\t') { cols += TabWidth + 1; if (col + cols > right) { cols = right - col; } for (int i = 0; i < cols; i++) { if (col + i < right) { AddRune (col + i, row, ' '); } } } else { AddRune (col, row, rune); } if (!TextModel.SetCol (ref col, bounds.Right, cols)) { break; } if (idxCol + 1 < lineRuneCount && col + Rune.ColumnWidth (line [idxCol + 1]) > right) { break; } } if (col < right) { ColorNormal (); ClearRegion (col, row, right, row + 1); } row++; } if (row < bottom) { ColorNormal (); ClearRegion (bounds.Left, row, right, bottom); } PositionCursor (); } /// public override bool CanFocus { get => base.CanFocus; set { base.CanFocus = value; } } void SetClipboard (ustring text) { if (text != null) { Clipboard.Contents = text; } } void AppendClipboard (ustring text) { Clipboard.Contents += text; } void Insert (Rune rune) { var line = GetCurrentLine (); if (Used) { line.Insert (Math.Min (currentColumn, line.Count), rune); } else { if (currentColumn < line.Count) { line.RemoveAt (currentColumn); } line.Insert (Math.Min (currentColumn, line.Count), rune); } if (wordWrap) { if (Used) { wrapNeeded = wrapManager.Insert (currentRow, currentColumn, rune); } else { wrapNeeded = wrapManager.RemoveAt (currentRow, currentColumn); wrapNeeded = wrapManager.Insert (currentRow, currentColumn, rune); } if (wrapNeeded) { SetNeedsDisplay (); } } var prow = currentRow - topRow; if (!wrapNeeded) { SetNeedsDisplay (new Rect (0, prow, Frame.Width, prow + 1)); } } ustring StringFromRunes (List runes) { if (runes == null) throw new ArgumentNullException (nameof (runes)); int size = 0; foreach (var rune in runes) { size += Utf8.RuneLen (rune); } var encoded = new byte [size]; int offset = 0; foreach (var rune in runes) { offset += Utf8.EncodeRune (rune, encoded, offset); } return ustring.Make (encoded); } List GetCurrentLine () => model.GetLine (currentRow); void InsertText (ustring text) { if (ustring.IsNullOrEmpty (text)) { return; } var lines = TextModel.StringToRunes (text); if (lines.Count == 0) { return; } var line = GetCurrentLine (); // Optimize single line if (lines.Count == 1) { line.InsertRange (currentColumn, lines [0]); currentColumn += lines [0].Count; if (!wordWrap && currentColumn - leftColumn > Frame.Width) { leftColumn = Math.Max (currentColumn - Frame.Width + 1, 0); } if (wordWrap) { SetNeedsDisplay (); } else { SetNeedsDisplay (new Rect (0, currentRow - topRow, Frame.Width, Math.Max (currentRow - topRow + 1, 0))); } return; } List rest = null; int lastp = 0; if (model.Count > 0 && currentColumn > 0) { // Keep a copy of the rest of the line var 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]); for (int i = 1; i < lines.Count; i++) { model.AddLine (currentRow + i, lines [i]); } if (rest != null) { var last = model.GetLine (currentRow + lines.Count - 1); lastp = last.Count; last.InsertRange (last.Count, rest); } // Now adjust column and row positions currentRow += lines.Count - 1; currentColumn = rest != null ? lastp : lines [lines.Count - 1].Count; Adjust (); } // The column we are tracking, or -1 if we are not tracking any column int columnTrack = -1; // Tries to snap the cursor to the tracking column void TrackColumn () { // Now track the column var line = GetCurrentLine (); if (line.Count < columnTrack) currentColumn = line.Count; else if (columnTrack != -1) currentColumn = columnTrack; else if (currentColumn > line.Count) currentColumn = line.Count; Adjust (); } void Adjust () { var offB = OffSetBackground (); var line = GetCurrentLine (); bool need = !NeedDisplay.IsEmpty || wrapNeeded; var tSize = TextModel.DisplaySize (line, -1, -1, false, TabWidth); var dSize = TextModel.DisplaySize (line, leftColumn, currentColumn, true, TabWidth); if (!wordWrap && currentColumn < leftColumn) { leftColumn = currentColumn; need = true; } else if (!wordWrap && (currentColumn - leftColumn + RightOffset > Frame.Width + offB.width || dSize.size + RightOffset >= Frame.Width + offB.width)) { leftColumn = TextModel.CalculateLeftColumn (line, leftColumn, currentColumn, Frame.Width + offB.width - RightOffset, TabWidth); need = true; } else if (dSize.size + RightOffset < Frame.Width + offB.width && tSize.size + RightOffset < Frame.Width + offB.width) { leftColumn = 0; } if (currentRow < topRow) { topRow = currentRow; need = true; } else if (currentRow - topRow + BottomOffset >= Frame.Height + offB.height) { topRow = Math.Min (Math.Max (currentRow - Frame.Height + 1 + BottomOffset, 0), currentRow); need = true; } if (need) { if (wrapNeeded) { WrapTextModel (); wrapNeeded = false; } SetNeedsDisplay (); } else { PositionCursor (); } } (int width, int height) OffSetBackground () { int w = 0; int h = 0; if (SuperView?.Frame.Right - Frame.Right < 0) { w = SuperView.Frame.Right - Frame.Right - 1; } if (SuperView?.Frame.Bottom - Frame.Bottom < 0) { h = SuperView.Frame.Bottom - Frame.Bottom - 1; } return (w, h); } /// /// 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) { var maxlength = model.GetMaxVisibleLine (topRow, topRow + Frame.Height + RightOffset, TabWidth); leftColumn = Math.Max (idx > maxlength - 1 ? maxlength - 1 : idx, 0); } SetNeedsDisplay (); } bool lastWasKill; bool wrapNeeded; bool shiftSelecting; /// public override bool ProcessKey (KeyEvent kb) { if (!CanFocus) { return true; } int restCount; List rest; // if the user presses Left (without any control keys) and they are at the start of the text if (kb.Key == Key.CursorLeft && currentColumn == 0 && currentRow == 0) { // do not respond (this lets the key press fall through to navigation system - which usually changes focus backward) return false; } // if the user presses Right (without any control keys) if (kb.Key == Key.CursorRight) { // determine where the last cursor position in the text is var lastRow = model.Count - 1; var 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) { return false; } } // Handle some state here - whether the last command was a kill // operation and the column tracking (up/down) switch (kb.Key) { case Key.N | Key.CtrlMask: case Key.CursorDown: case Key.P | Key.CtrlMask: case Key.CursorUp: lastWasKill = false; continuousFind = false; break; case Key.K | Key.CtrlMask: break; case Key.F | Key.CtrlMask: case Key.B | Key.CtrlMask: case (Key)((int)'B' + Key.AltMask): case Key.A | Key.CtrlMask: case Key.E | Key.CtrlMask: case Key.CursorRight: case Key.CursorLeft: case Key.CursorRight | Key.CtrlMask: case Key.CursorLeft | Key.CtrlMask: case Key.CursorRight | Key.ShiftMask: case Key.CursorLeft | Key.ShiftMask: case Key.CursorRight | Key.CtrlMask | Key.ShiftMask: case Key.CursorLeft | Key.CtrlMask | Key.ShiftMask: case Key.Home: case Key.Home | Key.CtrlMask: case Key.Home | Key.ShiftMask: case Key.Home | Key.CtrlMask | Key.ShiftMask: case Key.End: case Key.End | Key.CtrlMask: case Key.End | Key.ShiftMask: case Key.End | Key.CtrlMask | Key.ShiftMask: lastWasKill = false; columnTrack = -1; continuousFind = false; break; default: lastWasKill = false; columnTrack = -1; break; } // Dispatch the command. switch (kb.Key) { case Key.PageDown: case Key.V | Key.CtrlMask: case Key.PageDown | Key.ShiftMask: if (kb.Key.HasFlag (Key.ShiftMask)) { StartSelecting (); } else if (shiftSelecting && selecting) { StopSelecting (); } int nPageDnShift = Frame.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; SetNeedsDisplay (); } TrackColumn (); PositionCursor (); } break; case Key.PageUp: case ((int)'V' + Key.AltMask): case Key.PageUp | Key.ShiftMask: if (kb.Key.HasFlag (Key.ShiftMask)) { StartSelecting (); } else if (shiftSelecting && selecting) { StopSelecting (); } int nPageUpShift = Frame.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; SetNeedsDisplay (); } TrackColumn (); PositionCursor (); } break; case Key.N | Key.CtrlMask: case Key.CursorDown: case Key.CursorDown | Key.ShiftMask: if (kb.Key.HasFlag (Key.ShiftMask)) { StartSelecting (); } else if (shiftSelecting && selecting) { StopSelecting (); } MoveDown (); break; case Key.P | Key.CtrlMask: case Key.CursorUp: case Key.CursorUp | Key.ShiftMask: if (kb.Key.HasFlag (Key.ShiftMask)) { StartSelecting (); } else if (shiftSelecting && selecting) { StopSelecting (); } MoveUp (); break; case Key.F | Key.CtrlMask: case Key.CursorRight: case Key.CursorRight | Key.ShiftMask: if (kb.Key.HasFlag (Key.ShiftMask)) { StartSelecting (); } else if (shiftSelecting && selecting) { StopSelecting (); } var currentLine = GetCurrentLine (); if (currentColumn < currentLine.Count) { currentColumn++; } else { if (currentRow + 1 < model.Count) { currentRow++; currentColumn = 0; if (currentRow >= topRow + Frame.Height) { topRow++; SetNeedsDisplay (); } } } Adjust (); return true; case Key.B | Key.CtrlMask: case Key.CursorLeft: case Key.CursorLeft | Key.ShiftMask: if (kb.Key.HasFlag (Key.ShiftMask)) { StartSelecting (); } else if (shiftSelecting && selecting) { StopSelecting (); } if (currentColumn > 0) { currentColumn--; } else { if (currentRow > 0) { currentRow--; if (currentRow < topRow) { topRow--; SetNeedsDisplay (); } currentLine = GetCurrentLine (); currentColumn = currentLine.Count; } } Adjust (); break; case Key.Delete: case Key.Backspace: if (isReadOnly) break; if (selecting) { ClearSelectedRegion (); return true; } if (DeleteTextBackwards ()) { return true; } break; // Home, C-A case Key.Home: case Key.Home | Key.ShiftMask: case Key.A | Key.CtrlMask: if (kb.Key.HasFlag (Key.ShiftMask)) { StartSelecting (); } else if (shiftSelecting && selecting) { StopSelecting (); } currentColumn = 0; leftColumn = 0; Adjust (); break; case Key.DeleteChar: case Key.D | Key.CtrlMask: // Delete if (isReadOnly) break; if (selecting) { ClearSelectedRegion (); return true; } if (DeleteTextForwards ()) { return true; } break; case Key.End: case Key.End | Key.ShiftMask: case Key.E | Key.CtrlMask: // End if (kb.Key.HasFlag (Key.ShiftMask)) { StartSelecting (); } else if (shiftSelecting && selecting) { StopSelecting (); } currentLine = GetCurrentLine (); currentColumn = currentLine.Count; Adjust (); return true; case Key.K | Key.CtrlMask: // kill-to-end case Key.DeleteChar | Key.CtrlMask | Key.ShiftMask: if (isReadOnly) break; currentLine = GetCurrentLine (); var setLastWasKill = true; if (currentLine.Count > 0 && currentColumn == currentLine.Count) { DeleteTextForwards (); return true; } if (currentLine.Count == 0) { if (currentRow < model.Count - 1) { model.RemoveLine (currentRow); } if (model.Count > 0 || lastWasKill) { var val = ustring.Make ((Rune)'\n'); 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 { restCount = currentLine.Count - currentColumn; rest = currentLine.GetRange (currentColumn, restCount); var val = ustring.Empty; if (currentColumn == 0 && lastWasKill && currentLine.Count > 0) { val = ustring.Make ((Rune)'\n'); } val += StringFromRunes (rest); if (lastWasKill) { AppendClipboard (val); } else { SetClipboard (val); } currentLine.RemoveRange (currentColumn, restCount); } SetNeedsDisplay (new Rect (0, currentRow - topRow, Frame.Width, Frame.Height)); lastWasKill = setLastWasKill; break; case Key.Backspace | Key.CtrlMask | Key.ShiftMask: // kill-to-start if (isReadOnly) break; currentLine = GetCurrentLine (); setLastWasKill = true; if (currentLine.Count > 0 && currentColumn == 0) { DeleteTextBackwards (); return true; } if (currentLine.Count == 0) { if (currentRow > 0) { model.RemoveLine (currentRow); currentRow--; currentLine = model.GetLine (currentRow); currentColumn = currentLine.Count; } } else { restCount = currentColumn; rest = currentLine.GetRange (0, restCount); var val = ustring.Empty; if (currentColumn == 0 && lastWasKill && currentLine.Count > 0) { val = ustring.Make ((Rune)'\n'); } val += StringFromRunes (rest); if (lastWasKill) { AppendClipboard (val); } else { SetClipboard (val); } currentLine.RemoveRange (0, restCount); currentColumn = 0; } SetNeedsDisplay (new Rect (0, currentRow - topRow, Frame.Width, Frame.Height)); lastWasKill = setLastWasKill; break; case Key.Y | Key.CtrlMask: // Control-y, yank if (isReadOnly) break; Paste (); return true; case Key.Space | Key.CtrlMask: selecting = !selecting; selectionStartColumn = currentColumn; selectionStartRow = currentRow; break; case ((int)'C' + Key.AltMask): case Key.C | Key.CtrlMask: Copy (); return true; case ((int)'W' + Key.AltMask): case Key.W | Key.CtrlMask: case Key.X | Key.CtrlMask: Cut (); return true; case Key.CtrlMask | Key.CursorLeft: case Key.CtrlMask | Key.CursorLeft | Key.ShiftMask: case (Key)((int)'B' + Key.AltMask): if (kb.Key.HasFlag (Key.ShiftMask)) { StartSelecting (); } else if (shiftSelecting && selecting) { StopSelecting (); } var newPos = WordBackward (currentColumn, currentRow); if (newPos.HasValue) { currentColumn = newPos.Value.col; currentRow = newPos.Value.row; } Adjust (); break; case Key.CtrlMask | Key.CursorRight: case Key.CtrlMask | Key.CursorRight | Key.ShiftMask: case (Key)((int)'F' + Key.AltMask): if (kb.Key.HasFlag (Key.ShiftMask)) { StartSelecting (); } else if (shiftSelecting && selecting) { StopSelecting (); } newPos = WordForward (currentColumn, currentRow); if (newPos.HasValue) { currentColumn = newPos.Value.col; currentRow = newPos.Value.row; } Adjust (); break; case Key.DeleteChar | Key.CtrlMask: // kill-word-forwards if (isReadOnly) break; currentLine = GetCurrentLine (); if (currentLine.Count == 0 || currentColumn == currentLine.Count) { DeleteTextForwards (); return true; } newPos = WordForward (currentColumn, currentRow); 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 && wrapManager.RemoveRange (currentRow, currentColumn, restCount)) { wrapNeeded = true; } if (wrapNeeded) { SetNeedsDisplay (); } else { SetNeedsDisplay (new Rect (0, currentRow - topRow, Frame.Width, Frame.Height)); } break; case Key.Backspace | Key.CtrlMask: // kill-word-backwards if (isReadOnly) break; currentLine = GetCurrentLine (); if (currentColumn == 0) { DeleteTextBackwards (); return true; } newPos = WordBackward (currentColumn, currentRow); if (newPos.HasValue && currentRow == newPos.Value.row) { restCount = currentColumn - newPos.Value.col; currentLine.RemoveRange (newPos.Value.col, restCount); if (wordWrap && wrapManager.RemoveRange (currentRow, newPos.Value.col, restCount)) { wrapNeeded = true; } currentColumn = newPos.Value.col; } else if (newPos.HasValue) { restCount = currentLine.Count - currentColumn; currentLine.RemoveRange (currentColumn, restCount); if (wordWrap && wrapManager.RemoveRange (currentRow, currentColumn, restCount)) { wrapNeeded = true; } currentColumn = newPos.Value.col; currentRow = newPos.Value.row; } if (wrapNeeded) { SetNeedsDisplay (); } else { SetNeedsDisplay (new Rect (0, currentRow - topRow, Frame.Width, Frame.Height)); } break; case Key.Enter: if (!AllowsReturn) { return false; } if (isReadOnly) break; currentLine = GetCurrentLine (); restCount = currentLine.Count - currentColumn; rest = currentLine.GetRange (currentColumn, restCount); currentLine.RemoveRange (currentColumn, restCount); model.AddLine (currentRow + 1, rest); if (wordWrap) { wrapManager.AddLine (currentRow, currentColumn); wrapNeeded = true; } currentRow++; bool fullNeedsDisplay = false; if (currentRow >= topRow + Frame.Height) { topRow++; fullNeedsDisplay = true; } currentColumn = 0; if (!wordWrap && currentColumn < leftColumn) { fullNeedsDisplay = true; leftColumn = 0; } if (fullNeedsDisplay) SetNeedsDisplay (); else SetNeedsDisplay (new Rect (0, currentRow - topRow, 2, Frame.Height)); break; case Key.CtrlMask | Key.End: case Key.CtrlMask | Key.End | Key.ShiftMask: if (kb.Key.HasFlag (Key.ShiftMask)) { StartSelecting (); } else if (shiftSelecting && selecting) { StopSelecting (); } MoveEnd (); break; case Key.CtrlMask | Key.Home: case Key.CtrlMask | Key.Home | Key.ShiftMask: if (kb.Key.HasFlag (Key.ShiftMask)) { StartSelecting (); } else if (shiftSelecting && selecting) { StopSelecting (); } MoveHome (); break; case Key.T | Key.CtrlMask: SelectAll (); break; case Key.InsertChar: Used = !Used; SetNeedsDisplay (); break; case Key _ when ShortcutHelper.GetModifiersKey (kb) == Key.Tab: if (!AllowsTab) { return false; } InsertText (new KeyEvent ((Key)'\t', null)); break; case Key _ when (ShortcutHelper.GetModifiersKey (kb) == (Key.BackTab | Key.ShiftMask)): if (!AllowsTab) { return false; } if (currentColumn > 0) { currentLine = GetCurrentLine (); if (currentLine.Count > 0 && currentLine [currentColumn - 1] == '\t') { currentLine.RemoveAt (currentColumn - 1); currentColumn--; } } break; default: // Ignore control characters and other special keys if (kb.Key < Key.Space || kb.Key > Key.CharMask) return false; InsertText (kb); break; } DoNeededAction (); return true; } bool InsertText (KeyEvent kb) { //So that special keys like tab can be processed if (isReadOnly) return true; if (selecting) { ClearSelectedRegion (); } if (Used) { Insert ((uint)kb.Key); currentColumn++; if (currentColumn >= leftColumn + Frame.Width) { leftColumn++; SetNeedsDisplay (); } } else { Insert ((uint)kb.Key); currentColumn++; } return true; } /// public override bool OnKeyUp (KeyEvent kb) { switch (kb.Key) { case Key.Space | Key.CtrlMask: return true; } return false; } void DoNeededAction () { if (NeedDisplay.IsEmpty) { PositionCursor (); } else { Adjust (); } } bool DeleteTextForwards () { var currentLine = GetCurrentLine (); if (currentColumn == currentLine.Count) { if (currentRow + 1 == model.Count) return true; var nextLine = model.GetLine (currentRow + 1); currentLine.AddRange (nextLine); model.RemoveLine (currentRow + 1); if (wordWrap && wrapManager.RemoveLine (currentRow, currentColumn, out _)) { wrapNeeded = true; } var sr = currentRow - topRow; SetNeedsDisplay (new Rect (0, sr, Frame.Width, sr + 1)); } else { currentLine.RemoveAt (currentColumn); if (wordWrap && wrapManager.RemoveAt (currentRow, currentColumn)) { wrapNeeded = true; } var r = currentRow - topRow; SetNeedsDisplay (new Rect (currentColumn - leftColumn, r, Frame.Width, r + 1)); } return false; } bool DeleteTextBackwards () { if (currentColumn > 0) { // Delete backwards var currentLine = GetCurrentLine (); currentLine.RemoveAt (currentColumn - 1); if (wordWrap && wrapManager.RemoveAt (currentRow, currentColumn - 1)) { wrapNeeded = true; } currentColumn--; if (currentColumn < leftColumn) { leftColumn--; SetNeedsDisplay (); } else SetNeedsDisplay (new Rect (0, currentRow - topRow, 1, Frame.Width)); } else { // Merges the current line with the previous one. if (currentRow == 0) return true; var prowIdx = currentRow - 1; var prevRow = model.GetLine (prowIdx); var prevCount = prevRow.Count; model.GetLine (prowIdx).AddRange (GetCurrentLine ()); model.RemoveLine (currentRow); bool lineRemoved = false; if (wordWrap && wrapManager.RemoveLine (currentRow, currentColumn, out lineRemoved, false)) { wrapNeeded = true; } currentRow--; if (wrapNeeded && !lineRemoved) { currentColumn = Math.Max (prevCount - 1, 0); } else { currentColumn = prevCount; } Adjust (); } return false; } bool copyWithoutSelection; /// /// Copy the selected text to the clipboard contents. /// public void Copy () { SetWrapModel (); if (selecting) { SetClipboard (GetRegion ()); copyWithoutSelection = false; } else { var currentLine = GetCurrentLine (); SetClipboard (ustring.Make (currentLine)); copyWithoutSelection = true; } UpdateWrapModel (); DoNeededAction (); } /// /// Cut the selected text to the clipboard contents. /// public void Cut () { SetWrapModel (); SetClipboard (GetRegion ()); if (!isReadOnly) { ClearRegion (); } UpdateWrapModel (); selecting = false; DoNeededAction (); } /// /// Paste the clipboard contents into the current selected position. /// public void Paste () { if (isReadOnly) { return; } SetWrapModel (); if (copyWithoutSelection) { var runeList = Clipboard.Contents == null ? new List () : Clipboard.Contents.ToRuneList (); model.AddLine (currentRow, runeList); currentRow++; } else { if (selecting) { ClearRegion (); } InsertText (Clipboard.Contents); copyWithoutSelection = false; } UpdateWrapModel (); selecting = false; DoNeededAction (); } void StartSelecting () { if (shiftSelecting && selecting) { return; } shiftSelecting = true; selecting = true; selectionStartColumn = currentColumn; selectionStartRow = currentRow; } void StopSelecting () { shiftSelecting = false; selecting = false; } void ClearSelectedRegion () { SetWrapModel (); if (!isReadOnly) { ClearRegion (); } UpdateWrapModel (); selecting = false; DoNeededAction (); } void MoveUp () { if (currentRow > 0) { if (columnTrack == -1) { columnTrack = currentColumn; } currentRow--; if (currentRow < topRow) { topRow--; SetNeedsDisplay (); } TrackColumn (); PositionCursor (); } } void MoveDown () { if (currentRow + 1 < model.Count) { if (columnTrack == -1) { columnTrack = currentColumn; } currentRow++; if (currentRow + BottomOffset >= topRow + Frame.Height) { topRow++; SetNeedsDisplay (); } TrackColumn (); PositionCursor (); } else if (currentRow > Frame.Height) { Adjust (); } } IEnumerable<(int col, int row, Rune rune)> ForwardIterator (int col, int row) { if (col < 0 || row < 0) yield break; if (row >= model.Count) yield break; var 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 (); } } Rune RuneAt (int col, int row) { var line = model.GetLine (row); if (line.Count > 0) { return line [col > line.Count - 1 ? line.Count - 1 : col]; } else { return 0; } } /// /// Will scroll the to the last line and position the cursor there. /// public void MoveEnd () { currentRow = model.Count - 1; var line = GetCurrentLine (); currentColumn = line.Count; TrackColumn (); PositionCursor (); } /// /// Will scroll the to the first line and position the cursor there. /// public void MoveHome () { currentRow = 0; topRow = 0; currentColumn = 0; leftColumn = 0; TrackColumn (); PositionCursor (); } bool MoveNext (ref int col, ref int row, out Rune rune) { var line = model.GetLine (row); if (col + 1 < line.Count) { col++; rune = line [col]; if (col + 1 == line.Count) { col++; } return true; } while (row + 1 < model.Count) { col = 0; row++; line = model.GetLine (row); if (line.Count > 0) { rune = line [0]; return true; } } rune = 0; return false; } bool MovePrev (ref int col, ref int row, out Rune rune) { var line = model.GetLine (row); if (col > 0) { col--; rune = line [col]; return true; } if (row == 0) { rune = 0; return false; } while (row > 0) { row--; line = model.GetLine (row); col = line.Count - 1; if (col >= 0) { rune = line [col]; return true; } } rune = 0; return false; } (int col, int row)? WordForward (int fromCol, int fromRow) { var col = fromCol; var row = fromRow; try { var rune = RuneAt (col, row); void ProcMoveNext (ref int nCol, ref int nRow, Rune nRune) { if (Rune.IsSymbol (nRune) || Rune.IsWhiteSpace (nRune)) { while (MoveNext (ref nCol, ref nRow, out nRune)) { if (Rune.IsLetterOrDigit (nRune) || Rune.IsPunctuation (nRune)) return; } if (nRow != fromRow && (Rune.IsLetterOrDigit (nRune) || Rune.IsPunctuation (nRune))) { return; } while (MoveNext (ref nCol, ref nRow, out nRune)) { if (!Rune.IsLetterOrDigit (nRune) && !Rune.IsPunctuation (nRune)) break; } } else { if (!MoveNext (ref nCol, ref nRow, out nRune)) { return; } var line = model.GetLine (fromRow); if ((nRow != fromRow && fromCol < line.Count) || (nRow == fromRow && nCol == line.Count - 1)) { nCol = line.Count; nRow = fromRow; return; } else if (nRow != fromRow && fromCol == line.Count) { line = model.GetLine (nRow); if (Rune.IsLetterOrDigit (line [nCol]) || Rune.IsPunctuation (line [nCol])) { return; } } ProcMoveNext (ref nCol, ref nRow, nRune); } } ProcMoveNext (ref col, ref row, rune); if (fromCol != col || fromRow != row) return (col, row); return null; } catch (Exception) { return null; } } (int col, int row)? WordBackward (int fromCol, int fromRow) { if (fromRow == 0 && fromCol == 0) return null; var col = Math.Max (fromCol - 1, 0); var row = fromRow; try { var rune = RuneAt (col, row); int lastValidCol = Rune.IsLetterOrDigit (rune) || Rune.IsPunctuation (rune) ? col : -1; void ProcMovePrev (ref int nCol, ref int nRow, Rune nRune) { if (Rune.IsSymbol (nRune) || Rune.IsWhiteSpace (nRune)) { while (MovePrev (ref nCol, ref nRow, out nRune)) { if (Rune.IsLetterOrDigit (nRune) || Rune.IsPunctuation (nRune)) { lastValidCol = nCol; break; } } if (nRow != fromRow && (Rune.IsLetterOrDigit (nRune) || Rune.IsPunctuation (nRune))) { if (lastValidCol > -1) { nCol = lastValidCol; } return; } while (MovePrev (ref nCol, ref nRow, out nRune)) { if (!Rune.IsLetterOrDigit (nRune) && !Rune.IsPunctuation (nRune)) break; if (nRow != fromRow) { break; } lastValidCol = nCol; } if (lastValidCol > -1) { nCol = lastValidCol; nRow = fromRow; } } else { if (!MovePrev (ref nCol, ref nRow, out nRune)) { return; } var line = model.GetLine (nRow); if (nCol == 0 && nRow == fromRow && (Rune.IsLetterOrDigit (line [0]) || Rune.IsPunctuation (line [0]))) { return; } lastValidCol = Rune.IsLetterOrDigit (nRune) || Rune.IsPunctuation (nRune) ? nCol : lastValidCol; if (lastValidCol > -1 && (Rune.IsSymbol (nRune) || Rune.IsWhiteSpace (nRune))) { nCol = lastValidCol; return; } if (fromRow != nRow) { nCol = line.Count; return; } ProcMovePrev (ref nCol, ref nRow, nRune); } } ProcMovePrev (ref col, ref row, rune); if (fromCol != col || fromRow != row) return (col, row); return null; } catch (Exception) { return null; } } /// public override bool MouseEvent (MouseEvent ev) { if (!ev.Flags.HasFlag (MouseFlags.Button1Clicked) && !ev.Flags.HasFlag (MouseFlags.Button1Pressed) && !ev.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition) && !ev.Flags.HasFlag (MouseFlags.Button1Released) && !ev.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ButtonShift) && !ev.Flags.HasFlag (MouseFlags.WheeledDown) && !ev.Flags.HasFlag (MouseFlags.WheeledUp) && !ev.Flags.HasFlag (MouseFlags.Button1DoubleClicked) && !ev.Flags.HasFlag (MouseFlags.Button1DoubleClicked | MouseFlags.ButtonShift)) { return false; } if (!CanFocus) { return true; } if (!HasFocus) { SetFocus (); } continuousFind = false; if (ev.Flags == MouseFlags.Button1Clicked) { if (shiftSelecting) { shiftSelecting = false; selecting = false; } ProcessMouseClick (ev, out _); PositionCursor (); 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 && selecting) { if (currentRow - topRow + BottomOffset >= Frame.Height - 1 && model.Count + BottomOffset > topRow + currentRow) { ScrollTo (topRow + Frame.Height); } else if (topRow > 0 && currentRow <= topRow) { ScrollTo (topRow - Frame.Height); } else if (ev.Y >= Frame.Height) { ScrollTo (model.Count + BottomOffset); } else if (ev.Y < 0 && topRow > 0) { ScrollTo (0); } if (currentColumn - leftColumn + RightOffset >= Frame.Width - 1 && line.Count + RightOffset > leftColumn + currentColumn) { ScrollTo (leftColumn + Frame.Width, false); } else if (leftColumn > 0 && currentColumn <= leftColumn) { ScrollTo (leftColumn - Frame.Width, false); } else if (ev.X >= Frame.Width) { ScrollTo (line.Count + RightOffset, false); } else if (ev.X < 0 && leftColumn > 0) { ScrollTo (0, false); } } lastWasKill = false; columnTrack = currentColumn; } else if (ev.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ButtonShift)) { if (!shiftSelecting) { StartSelecting (); } ProcessMouseClick (ev, out _); PositionCursor (); lastWasKill = false; columnTrack = currentColumn; } else if (ev.Flags.HasFlag (MouseFlags.Button1Pressed)) { if (shiftSelecting) { shiftSelecting = false; selecting = false; } ProcessMouseClick (ev, out _); PositionCursor (); if (!selecting) { StartSelecting (); } lastWasKill = false; columnTrack = currentColumn; if (Application.mouseGrabView == null) { Application.GrabMouse (this); } } else if (ev.Flags.HasFlag (MouseFlags.Button1Released)) { Application.UngrabMouse (); } else if (ev.Flags.HasFlag (MouseFlags.Button1DoubleClicked)) { if (ev.Flags.HasFlag (MouseFlags.ButtonShift)) { if (!selecting) { StartSelecting (); } } else if (selecting) { StopSelecting (); } ProcessMouseClick (ev, out List line); (int col, int row)? newPos = null; if (currentColumn > 0 && line [currentColumn - 1] != ' ') { newPos = WordBackward (currentColumn, currentRow); if (newPos.HasValue) { currentColumn = newPos.Value.col; currentRow = newPos.Value.row; } } if (!selecting) { StartSelecting (); } if (currentRow < selectionStartRow || currentRow == selectionStartRow && currentColumn < selectionStartColumn) { if (currentColumn > 0 && line [currentColumn - 1] != ' ') { newPos = WordBackward (currentColumn, currentRow); } } else { newPos = WordForward (currentColumn, currentRow); } if (newPos != null && newPos.HasValue) { currentColumn = newPos.Value.col; currentRow = newPos.Value.row; } PositionCursor (); lastWasKill = false; columnTrack = currentColumn; } return true; } void ProcessMouseClick (MouseEvent ev, out List line) { List r = null; if (model.Count > 0) { var maxCursorPositionableLine = Math.Max ((model.Count - 1) - topRow, 0); if (Math.Max (ev.Y, 0) > maxCursorPositionableLine) { currentRow = maxCursorPositionableLine + topRow; } else { currentRow = Math.Max (ev.Y + topRow, 0); } r = GetCurrentLine (); var idx = TextModel.GetColFromX (r, leftColumn, Math.Max (ev.X, 0), TabWidth); if (idx - leftColumn >= r.Count + RightOffset) { currentColumn = Math.Max (r.Count - leftColumn + RightOffset, 0); } else { currentColumn = idx + leftColumn; } } line = r; } /// public override bool OnLeave (View view) { if (Application.mouseGrabView != null && Application.mouseGrabView == this) { Application.UngrabMouse (); } return base.OnLeave (view); } } }