// TextView.cs: multi-line text editing using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Text; using System.Threading; using NStack; using Terminal.Gui.Resources; using static Terminal.Gui.Graphs.PathAnnotation; using Rune = System.Rune; namespace Terminal.Gui { class TextModel { List> lines = new List> (); public event Action LinesLoaded; 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 (); OnLinesLoaded (); } public void LoadString (ustring content) { lines = StringToRunes (content); OnLinesLoaded (); } void OnLinesLoaded () { LinesLoaded?.Invoke (); } 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); } } public void ReplaceLine (int pos, List runes) { if (lines.Count > 0 && pos < lines.Count) { lines [pos] = new List (runes); } else if (lines.Count == 0 || (lines.Count > 0 && pos >= lines.Count)) { lines.Add (runes); } } /// /// 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) { if (col + width == end) { col++; } break; } else if ((end < t.Count && col > 0 && start < end && col == start) || (end - col == width - 1)) { 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 < 0 || 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 HistoryText { public enum LineStatus { Original, Replaced, Removed, Added } public class HistoryTextItem { public List> Lines; public Point CursorPosition; public LineStatus LineStatus; public bool IsUndoing; public Point FinalCursorPosition; public HistoryTextItem RemovedOnAdded; public HistoryTextItem (List> lines, Point curPos, LineStatus linesStatus) { Lines = lines; CursorPosition = curPos; LineStatus = linesStatus; } public HistoryTextItem (HistoryTextItem historyTextItem) { Lines = new List> (historyTextItem.Lines); CursorPosition = new Point (historyTextItem.CursorPosition.X, historyTextItem.CursorPosition.Y); LineStatus = historyTextItem.LineStatus; } public override string ToString () { return $"(Count: {Lines.Count}, Cursor: {CursorPosition}, Status: {LineStatus})"; } } List historyTextItems = new List (); int idxHistoryText = -1; ustring originalText; public bool IsFromHistory { get; private set; } public bool HasHistoryChanges => idxHistoryText > -1; public event Action ChangeText; public void Add (List> lines, Point curPos, LineStatus lineStatus = LineStatus.Original) { if (lineStatus == LineStatus.Original && historyTextItems.Count > 0 && historyTextItems.Last ().LineStatus == LineStatus.Original) { return; } if (lineStatus == LineStatus.Replaced && historyTextItems.Count > 0 && historyTextItems.Last ().LineStatus == LineStatus.Replaced) { return; } if (historyTextItems.Count == 0 && lineStatus != LineStatus.Original) throw new ArgumentException ("The first item must be the original."); if (idxHistoryText >= 0 && idxHistoryText + 1 < historyTextItems.Count) historyTextItems.RemoveRange (idxHistoryText + 1, historyTextItems.Count - idxHistoryText - 1); historyTextItems.Add (new HistoryTextItem (lines, curPos, lineStatus)); idxHistoryText++; } public void ReplaceLast (List> lines, Point curPos, LineStatus lineStatus) { var found = historyTextItems.FindLast (x => x.LineStatus == lineStatus); if (found != null) { found.Lines = lines; found.CursorPosition = curPos; } } public void Undo () { if (historyTextItems?.Count > 0 && idxHistoryText > 0) { IsFromHistory = true; idxHistoryText--; var historyTextItem = new HistoryTextItem (historyTextItems [idxHistoryText]) { IsUndoing = true }; ProcessChanges (ref historyTextItem); IsFromHistory = false; } } public void Redo () { if (historyTextItems?.Count > 0 && idxHistoryText < historyTextItems.Count - 1) { IsFromHistory = true; idxHistoryText++; var historyTextItem = new HistoryTextItem (historyTextItems [idxHistoryText]) { IsUndoing = false }; ProcessChanges (ref historyTextItem); IsFromHistory = false; } } void ProcessChanges (ref HistoryTextItem historyTextItem) { if (historyTextItem.IsUndoing) { if (idxHistoryText - 1 > -1 && ((historyTextItems [idxHistoryText - 1].LineStatus == LineStatus.Added) || historyTextItems [idxHistoryText - 1].LineStatus == LineStatus.Removed || (historyTextItem.LineStatus == LineStatus.Replaced && historyTextItems [idxHistoryText - 1].LineStatus == LineStatus.Original))) { idxHistoryText--; while (historyTextItems [idxHistoryText].LineStatus == LineStatus.Added && historyTextItems [idxHistoryText - 1].LineStatus == LineStatus.Removed) { idxHistoryText--; } historyTextItem = new HistoryTextItem (historyTextItems [idxHistoryText]); historyTextItem.IsUndoing = true; historyTextItem.FinalCursorPosition = historyTextItem.CursorPosition; } if (historyTextItem.LineStatus == LineStatus.Removed && historyTextItems [idxHistoryText + 1].LineStatus == LineStatus.Added) { historyTextItem.RemovedOnAdded = new HistoryTextItem (historyTextItems [idxHistoryText + 1]); } if ((historyTextItem.LineStatus == LineStatus.Added && historyTextItems [idxHistoryText - 1].LineStatus == LineStatus.Original) || (historyTextItem.LineStatus == LineStatus.Removed && historyTextItems [idxHistoryText - 1].LineStatus == LineStatus.Original) || (historyTextItem.LineStatus == LineStatus.Added && historyTextItems [idxHistoryText - 1].LineStatus == LineStatus.Removed)) { if (!historyTextItem.Lines [0].SequenceEqual (historyTextItems [idxHistoryText - 1].Lines [0]) && historyTextItem.CursorPosition == historyTextItems [idxHistoryText - 1].CursorPosition) { historyTextItem.Lines [0] = new List (historyTextItems [idxHistoryText - 1].Lines [0]); } if (historyTextItem.LineStatus == LineStatus.Added && historyTextItems [idxHistoryText - 1].LineStatus == LineStatus.Removed) { historyTextItem.FinalCursorPosition = historyTextItems [idxHistoryText - 2].CursorPosition; } else { historyTextItem.FinalCursorPosition = historyTextItems [idxHistoryText - 1].CursorPosition; } } else { historyTextItem.FinalCursorPosition = historyTextItem.CursorPosition; } OnChangeText (historyTextItem); while (historyTextItems [idxHistoryText].LineStatus == LineStatus.Removed || historyTextItems [idxHistoryText].LineStatus == LineStatus.Added) { idxHistoryText--; } } else if (!historyTextItem.IsUndoing) { if (idxHistoryText + 1 < historyTextItems.Count && (historyTextItem.LineStatus == LineStatus.Original || historyTextItems [idxHistoryText + 1].LineStatus == LineStatus.Added || historyTextItems [idxHistoryText + 1].LineStatus == LineStatus.Removed)) { idxHistoryText++; historyTextItem = new HistoryTextItem (historyTextItems [idxHistoryText]); historyTextItem.IsUndoing = false; historyTextItem.FinalCursorPosition = historyTextItem.CursorPosition; } if (historyTextItem.LineStatus == LineStatus.Added && historyTextItems [idxHistoryText - 1].LineStatus == LineStatus.Removed) { historyTextItem.RemovedOnAdded = new HistoryTextItem (historyTextItems [idxHistoryText - 1]); } if ((historyTextItem.LineStatus == LineStatus.Removed && historyTextItems [idxHistoryText + 1].LineStatus == LineStatus.Replaced) || (historyTextItem.LineStatus == LineStatus.Removed && historyTextItems [idxHistoryText + 1].LineStatus == LineStatus.Original) || (historyTextItem.LineStatus == LineStatus.Added && historyTextItems [idxHistoryText + 1].LineStatus == LineStatus.Replaced)) { if (historyTextItem.LineStatus == LineStatus.Removed && !historyTextItem.Lines [0].SequenceEqual (historyTextItems [idxHistoryText + 1].Lines [0])) { historyTextItem.Lines [0] = new List (historyTextItems [idxHistoryText + 1].Lines [0]); } historyTextItem.FinalCursorPosition = historyTextItems [idxHistoryText + 1].CursorPosition; } else { historyTextItem.FinalCursorPosition = historyTextItem.CursorPosition; } OnChangeText (historyTextItem); while (historyTextItems [idxHistoryText].LineStatus == LineStatus.Removed || historyTextItems [idxHistoryText].LineStatus == LineStatus.Added) { idxHistoryText++; } } } void OnChangeText (HistoryTextItem lines) { ChangeText?.Invoke (lines); } public void Clear (ustring text) { historyTextItems.Clear (); idxHistoryText = -1; originalText = text; OnChangeText (null); } public bool IsDirty (ustring text) { return originalText != text; } } 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, bool preserveTrailingSpaces = true) { 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, preserveTrailingSpaces, 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 <= Math.Min (line, wrappedModelLines.Count - 1); 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; } if (modelCol < line.Count) 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, bool preserveTrailingSpaces) { isWrapModelRefreshing = true; Model = model; WrapModel (frameWidth, out nRow, out nCol, out nStartRow, out nStartCol, row, col, startRow, startCol, tabWidth: 0, preserveTrailingSpaces); isWrapModelRefreshing = false; } public int GetWrappedLineColWidth (int line, int col, WordWrapManager wrapManager) { if (wrappedModelLines?.Count == 0) return 0; var wModelLines = wrapManager.wrappedModelLines; var modelLine = GetModelLineFromWrappedLines (line); var firstLine = wrappedModelLines.IndexOf (r => r.ModelLine == modelLine); int modelCol = 0; int colWidthOffset = 0; int i = firstLine; while (modelCol < col) { var wLine = wrappedModelLines [i]; var wLineToCompare = wModelLines [i]; if (wLine.ModelLine != modelLine || wLineToCompare.ModelLine != modelLine) break; modelCol += Math.Max (wLine.ColWidth, wLineToCompare.ColWidth); colWidthOffset += wLine.ColWidth - wLineToCompare.ColWidth; if (modelCol > col) { modelCol += col - modelCol; } i++; } return modelCol - colWidthOffset; } } /// /// Multi-line text editing . /// /// /// /// provides a multi-line text editor. Users interact /// with it with the standard Windows, Mac, and Linux (Emacs) commands. /// /// /// /// 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; HistoryText historyText = new HistoryText (); CultureInfo currentCulture; /// /// Raised when the property of the changes. /// /// /// The property of only changes when it is explicitly /// set, not as the user types. To be notified as the user changes the contents of the TextView /// see . /// public event Action TextChanged; /// /// Raised when the contents of the are changed. /// /// /// Unlike the event, this event is raised whenever the user types or /// otherwise changes the contents of the . /// public event Action ContentsChanged; /// /// Invoked with the unwrapped . /// public event Action UnwrappedCursorPosition; /// /// Provides autocomplete context menu based on suggestions at the current cursor /// position. Populate to enable this feature /// public IAutocomplete Autocomplete { get; protected set; } = new TextViewAutocomplete (); /// /// 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; model.LinesLoaded += Model_LinesLoaded; historyText.ChangeText += HistoryText_ChangeText; Initialized += TextView_Initialized; // Things this view knows how to do 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.LineDown, () => { ProcessMoveDown (); return true; }); AddCommand (Command.LineDownExtend, () => { ProcessMoveDownExtend (); return true; }); AddCommand (Command.LineUp, () => { ProcessMoveUp (); return true; }); AddCommand (Command.LineUpExtend, () => { 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.StartOfLine, () => { ProcessMoveStartOfLine (); return true; }); AddCommand (Command.StartOfLineExtend, () => { ProcessMoveStartOfLineExtend (); return true; }); AddCommand (Command.DeleteCharRight, () => { ProcessDeleteCharRight (); return true; }); AddCommand (Command.EndOfLine, () => { ProcessMoveEndOfLine (); return true; }); AddCommand (Command.EndOfLineExtend, () => { ProcessMoveEndOfLineExtend (); return true; }); AddCommand (Command.CutToEndLine, () => { KillToEndOfLine (); return true; }); AddCommand (Command.CutToStartLine, () => { KillToStartOfLine (); 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.NewLine, () => ProcessReturn ()); AddCommand (Command.BottomEnd, () => { MoveBottomEnd (); return true; }); AddCommand (Command.BottomEndExtend, () => { MoveBottomEndExtend (); return true; }); AddCommand (Command.TopHome, () => { MoveTopHome (); return true; }); AddCommand (Command.TopHomeExtend, () => { 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.NextView, () => ProcessMoveNextView ()); AddCommand (Command.PreviousView, () => ProcessMovePreviousView ()); AddCommand (Command.Undo, () => { UndoChanges (); return true; }); AddCommand (Command.Redo, () => { RedoChanges (); return true; }); AddCommand (Command.DeleteAll, () => { DeleteAll (); return true; }); AddCommand (Command.Accept, () => { ContextMenu.Position = new Point (CursorPosition.X - leftColumn + 2, CursorPosition.Y - topRow + 2); ShowContextMenu (); return true; }); // Default keybindings for this view AddKeyBinding (Key.PageDown, Command.PageDown); AddKeyBinding (Key.V | Key.CtrlMask, Command.PageDown); AddKeyBinding (Key.PageDown | Key.ShiftMask, Command.PageDownExtend); AddKeyBinding (Key.PageUp, Command.PageUp); AddKeyBinding (((int)'V' + Key.AltMask), Command.PageUp); AddKeyBinding (Key.PageUp | Key.ShiftMask, Command.PageUpExtend); AddKeyBinding (Key.N | Key.CtrlMask, Command.LineDown); AddKeyBinding (Key.CursorDown, Command.LineDown); AddKeyBinding (Key.CursorDown | Key.ShiftMask, Command.LineDownExtend); AddKeyBinding (Key.P | Key.CtrlMask, Command.LineUp); AddKeyBinding (Key.CursorUp, Command.LineUp); AddKeyBinding (Key.CursorUp | Key.ShiftMask, Command.LineUpExtend); AddKeyBinding (Key.F | Key.CtrlMask, Command.Right); AddKeyBinding (Key.CursorRight, Command.Right); AddKeyBinding (Key.CursorRight | Key.ShiftMask, Command.RightExtend); AddKeyBinding (Key.B | Key.CtrlMask, Command.Left); AddKeyBinding (Key.CursorLeft, Command.Left); AddKeyBinding (Key.CursorLeft | Key.ShiftMask, Command.LeftExtend); AddKeyBinding (Key.Delete, Command.DeleteCharLeft); AddKeyBinding (Key.Backspace, Command.DeleteCharLeft); AddKeyBinding (Key.Home, Command.StartOfLine); AddKeyBinding (Key.A | Key.CtrlMask, Command.StartOfLine); AddKeyBinding (Key.Home | Key.ShiftMask, Command.StartOfLineExtend); AddKeyBinding (Key.DeleteChar, Command.DeleteCharRight); AddKeyBinding (Key.D | Key.CtrlMask, Command.DeleteCharRight); AddKeyBinding (Key.End, Command.EndOfLine); AddKeyBinding (Key.E | Key.CtrlMask, Command.EndOfLine); AddKeyBinding (Key.End | Key.ShiftMask, Command.EndOfLineExtend); AddKeyBinding (Key.K | Key.CtrlMask, Command.CutToEndLine); // kill-to-end AddKeyBinding (Key.DeleteChar | Key.CtrlMask | Key.ShiftMask, Command.CutToEndLine); // kill-to-end AddKeyBinding (Key.K | Key.AltMask, Command.CutToStartLine); // kill-to-start AddKeyBinding (Key.Backspace | Key.CtrlMask | Key.ShiftMask, Command.CutToStartLine); // kill-to-start AddKeyBinding (Key.Y | Key.CtrlMask, Command.Paste); // Control-y, yank AddKeyBinding (Key.Space | Key.CtrlMask, Command.ToggleExtend); AddKeyBinding (((int)'C' + Key.AltMask), Command.Copy); AddKeyBinding (Key.C | Key.CtrlMask, Command.Copy); AddKeyBinding (((int)'W' + Key.AltMask), Command.Cut); AddKeyBinding (Key.W | Key.CtrlMask, Command.Cut); AddKeyBinding (Key.X | Key.CtrlMask, Command.Cut); AddKeyBinding (Key.CursorLeft | Key.CtrlMask, Command.WordLeft); AddKeyBinding ((Key)((int)'B' + Key.AltMask), Command.WordLeft); AddKeyBinding (Key.CursorLeft | Key.CtrlMask | Key.ShiftMask, Command.WordLeftExtend); AddKeyBinding (Key.CursorRight | Key.CtrlMask, Command.WordRight); AddKeyBinding ((Key)((int)'F' + Key.AltMask), Command.WordRight); AddKeyBinding (Key.CursorRight | Key.CtrlMask | Key.ShiftMask, Command.WordRightExtend); AddKeyBinding (Key.DeleteChar | Key.CtrlMask, Command.KillWordForwards); // kill-word-forwards AddKeyBinding (Key.Backspace | Key.CtrlMask, Command.KillWordBackwards); // kill-word-backwards AddKeyBinding (Key.Enter, Command.NewLine); AddKeyBinding (Key.End | Key.CtrlMask, Command.BottomEnd); AddKeyBinding (Key.End | Key.CtrlMask | Key.ShiftMask, Command.BottomEndExtend); AddKeyBinding (Key.Home | Key.CtrlMask, Command.TopHome); AddKeyBinding (Key.Home | Key.CtrlMask | Key.ShiftMask, Command.TopHomeExtend); AddKeyBinding (Key.T | Key.CtrlMask, Command.SelectAll); AddKeyBinding (Key.InsertChar, Command.ToggleOverwrite); AddKeyBinding (Key.Tab, Command.Tab); AddKeyBinding (Key.BackTab | Key.ShiftMask, Command.BackTab); AddKeyBinding (Key.Tab | Key.CtrlMask, Command.NextView); AddKeyBinding (Application.AlternateForwardKey, Command.NextView); AddKeyBinding (Key.Tab | Key.CtrlMask | Key.ShiftMask, Command.PreviousView); AddKeyBinding (Application.AlternateBackwardKey, Command.PreviousView); AddKeyBinding (Key.Z | Key.CtrlMask, Command.Undo); AddKeyBinding (Key.R | Key.CtrlMask, Command.Redo); AddKeyBinding (Key.G | Key.CtrlMask, Command.DeleteAll); AddKeyBinding (Key.D | Key.CtrlMask | Key.ShiftMask, Command.DeleteAll); currentCulture = Thread.CurrentThread.CurrentUICulture; ContextMenu = new ContextMenu () { MenuItems = BuildContextMenuBarItem () }; ContextMenu.KeyChanged += ContextMenu_KeyChanged; AddKeyBinding (ContextMenu.Key, Command.Accept); } private MenuBarItem BuildContextMenuBarItem () { return new MenuBarItem (new MenuItem [] { new MenuItem (Strings.ctxSelectAll, "", () => SelectAll (), null, null, GetKeyFromCommand (Command.SelectAll)), new MenuItem (Strings.ctxDeleteAll, "", () => DeleteAll (), null, null, GetKeyFromCommand (Command.DeleteAll)), new MenuItem (Strings.ctxCopy, "", () => Copy (), null, null, GetKeyFromCommand (Command.Copy)), new MenuItem (Strings.ctxCut, "", () => Cut (), null, null, GetKeyFromCommand (Command.Cut)), new MenuItem (Strings.ctxPaste, "", () => Paste (), null, null, GetKeyFromCommand (Command.Paste)), new MenuItem (Strings.ctxUndo, "", () => UndoChanges (), null, null, GetKeyFromCommand (Command.Undo)), new MenuItem (Strings.ctxRedo, "", () => RedoChanges (), null, null, GetKeyFromCommand (Command.Redo)), }); } private void ContextMenu_KeyChanged (Key obj) { ReplaceKeyBinding (obj, ContextMenu.Key); } private void Model_LinesLoaded () { // 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); } private void HistoryText_ChangeText (HistoryText.HistoryTextItem obj) { SetWrapModel (); if (obj != null) { var startLine = obj.CursorPosition.Y; if (obj.RemovedOnAdded != null) { int offset; if (obj.IsUndoing) { offset = Math.Max (obj.RemovedOnAdded.Lines.Count - obj.Lines.Count, 1); } else { offset = obj.RemovedOnAdded.Lines.Count - 1; } for (int i = 0; i < offset; i++) { if (Lines > obj.RemovedOnAdded.CursorPosition.Y) { model.RemoveLine (obj.RemovedOnAdded.CursorPosition.Y); } else { break; } } } for (int i = 0; i < obj.Lines.Count; i++) { if (i == 0) { model.ReplaceLine (startLine, obj.Lines [i]); } else if ((obj.IsUndoing && obj.LineStatus == HistoryText.LineStatus.Removed) || !obj.IsUndoing && obj.LineStatus == HistoryText.LineStatus.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 (); } void TextView_Initialized (object sender, EventArgs e) { Autocomplete.HostControl = this; if (Application.Top != null) { Application.Top.AlternateForwardKeyChanged += Top_AlternateForwardKeyChanged; Application.Top.AlternateBackwardKeyChanged += Top_AlternateBackwardKeyChanged; } OnContentsChanged (); } void Top_AlternateBackwardKeyChanged (Key obj) { ReplaceKeyBinding (obj, Application.AlternateBackwardKey); } void Top_AlternateForwardKeyChanged (Key obj) { ReplaceKeyBinding (obj, Application.AlternateForwardKey); } /// /// 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; StopSelecting (); ResetCursorVisibility (); } /// /// Sets or gets the text in the . /// /// /// The event is fired whenever this property is set. Note, however, /// that Text is not set by as the user types. /// public override 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 (frameWidth, out _, out _, out _, out _); } TextChanged?.Invoke (); SetNeedsDisplay (); historyText.Clear (Text); } } /// public override Rect Frame { get => base.Frame; set { base.Frame = value; WrapTextModel (); Adjust (); } } void WrapTextModel () { if (wordWrap && wrapManager != null) { model = wrapManager.WrapModel (frameWidth, out int nRow, out int nCol, out int nStartRow, out int nStartCol, currentRow, currentColumn, selectionStartRow, selectionStartColumn, tabWidth, preserveTrailingSpaces: true); currentRow = nRow; currentColumn = nCol; selectionStartRow = nStartRow; selectionStartColumn = nStartCol; SetNeedsDisplay (); } } int frameWidth => Math.Max (Frame.Width - (RightOffset != 0 ? 2 : 1), 0); /// /// 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 { if (value > 0 && wordWrap) return; 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; } return GetSelectedRegion (); } } /// /// 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; } if (value && !multiline) { return; } wordWrap = value; ResetPosition (); if (wordWrap) { wrapManager = new WordWrapManager (model); model = wrapManager.WrapModel (frameWidth, 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 { topRow = AdjustOffset (value); bottomOffset = value; } } /// /// 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 { leftColumn = AdjustOffset (value, false); rightOffset = value; } } /// /// 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 whether the inserts a tab character into the text or ignores /// tab input. If set to `false` and the user presses the tab key (or shift-tab) the focus will move to the /// next view (or previous with shift-tab). The default is `true`; if the user presses the tab key, a tab /// character will be inserted into the text. /// public bool AllowsTab { get => allowsTab; set { allowsTab = value; if (allowsTab && tabWidth == 0) { tabWidth = 4; } if (allowsTab && !multiline) { Multiline = true; } if (!allowsTab && tabWidth > 0) { tabWidth = 0; } 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; WordWrap = false; currentColumn = 0; currentRow = 0; savedHeight = Height; var lyout = LayoutStyle; if (LayoutStyle == LayoutStyle.Computed) { LayoutStyle = LayoutStyle.Absolute; } Height = 1; LayoutStyle = lyout; Autocomplete.PopupInsideContainer = false; SetNeedsDisplay (); } else if (multiline && savedHeight != null) { var lyout = LayoutStyle; if (LayoutStyle == LayoutStyle.Computed) { LayoutStyle = LayoutStyle.Absolute; } Height = savedHeight; LayoutStyle = lyout; Autocomplete.PopupInsideContainer = true; SetNeedsDisplay (); } } } /// /// Indicates whatever the text was changed or not. /// if the text was changed otherwise. /// public bool IsDirty => historyText.IsDirty (Text); /// /// Indicates whatever the text has history changes or not. /// if the text has history changes otherwise. /// public bool HasHistoryChanges => historyText.HasHistoryChanges; /// /// Get the for this view. /// public ContextMenu ContextMenu { get; private set; } 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) { DesiredCursorVisibility = savedCursorVisibility; savedCursorVisibility = 0; } } /// /// 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) { bool res; try { SetWrapModel (); res = model.LoadFile (path); historyText.Clear (Text); ResetPosition (); } catch (Exception) { throw; } finally { UpdateWrapModel (); SetNeedsDisplay (); Adjust (); } 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); historyText.Clear (Text); 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; } } } var posX = currentColumn - leftColumn; var posY = currentRow - topRow; if (posX > -1 && col >= posX && posX < Frame.Width - RightOffset && topRow <= currentRow && posY < Frame.Height - BottomOffset) { 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 SetNormalColor () { 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 SetNormalColor (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 SetSelectionColor (List line, int idx) { Driver.SetAttribute (new Attribute (ColorScheme.Focus.Background, ColorScheme.Focus.Foreground)); } /// /// 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 SetReadOnlyColor (List line, int idx) { Attribute attribute; if (ColorScheme.Disabled.Foreground == ColorScheme.Focus.Background) { attribute = new Attribute (ColorScheme.Focus.Foreground, ColorScheme.Focus.Background); } else { attribute = new Attribute (ColorScheme.Disabled.Foreground, ColorScheme.Focus.Background); } Driver.SetAttribute (attribute); } /// /// 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 SetUsedColor (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 { if (value != isReadOnly) { isReadOnly = value; SetNeedsDisplay (); Adjust (); } } } CursorVisibility desiredCursorVisibility = CursorVisibility.Default; /// /// Get / Set the wished cursor when the field is focused /// public CursorVisibility DesiredCursorVisibility { get => desiredCursorVisibility; set { if (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); } /// public override bool OnLeave (View view) { if (Application.MouseGrabView != null && Application.MouseGrabView == this) { Application.UngrabMouse (); } return base.OnLeave (view); } // Returns an encoded region start..end (top 32 bits are the row, low32 the column) 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 == null || startCol == null || cRow == null || cCol == 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; } } 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 (int? sRow = null, int? sCol = null, int? cRow = null, int? cCol = null, TextModel model = null) { long start, end; GetEncodedRegionBounds (out start, out end, sRow, sCol, cRow, cCol); 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 == null ? this.model.GetLine (startRow) : 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 (Environment.NewLine) + StringFromRunes (model == null ? this.model.GetLine (row) : model.GetLine (row)); } line = model == null ? this.model.GetLine (maxrow) : model.GetLine (maxrow); res = res + ustring.Make (Environment.NewLine) + StringFromRunes (line.GetRange (0, endCol)); return res; } // // Clears the contents of the selected region // void ClearRegion () { SetWrapModel (); 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); historyText.Add (new List> () { new List (line) }, new Point (startCol, startRow)); List> removedLines = new List> (); if (startRow == maxrow) { removedLines.Add (new List (line)); line.RemoveRange (startCol, endCol - startCol); currentColumn = startCol; if (wordWrap) { SetNeedsDisplay (); } else { SetNeedsDisplay (new Rect (0, startRow - topRow, Frame.Width, startRow - topRow + 1)); } historyText.Add (new List> (removedLines), CursorPosition, HistoryText.LineStatus.Removed); UpdateWrapModel (); return; } removedLines.Add (new List (line)); line.RemoveRange (startCol, line.Count - startCol); var line2 = model.GetLine (maxrow); line.AddRange (line2.Skip (endCol)); for (int row = startRow + 1; row <= maxrow; row++) { removedLines.Add (new List (model.GetLine (startRow + 1))); model.RemoveLine (startRow + 1); } if (currentEncoded == end) { currentRow -= maxrow - (startRow); } currentColumn = startCol; historyText.Add (new List> (removedLines), CursorPosition, HistoryText.LineStatus.Removed); UpdateWrapModel (); 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)); } } string currentCaller; /// /// Restore from original model. /// void SetWrapModel ([CallerMemberName] string caller = null) { if (currentCaller != null) return; if (wordWrap) { currentCaller = caller; currentColumn = wrapManager.GetModelColFromWrappedLines (currentRow, currentColumn); currentRow = wrapManager.GetModelLineFromWrappedLines (currentRow); selectionStartColumn = wrapManager.GetModelColFromWrappedLines (selectionStartRow, selectionStartColumn); selectionStartRow = wrapManager.GetModelLineFromWrappedLines (selectionStartRow); model = wrapManager.Model; } } /// /// Update the original model. /// void UpdateWrapModel ([CallerMemberName] string caller = null) { if (currentCaller != null && 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, preserveTrailingSpaces: true); currentRow = nRow; currentColumn = nCol; selectionStartRow = nStartRow; selectionStartColumn = nStartCol; wrapNeeded = true; } if (currentCaller != null) throw new InvalidOperationException ($"WordWrap settings was changed after the {currentCaller} call."); } /// /// Invoke the event with the unwrapped . /// public virtual void OnUnwrappedCursorPosition (int? cRow = null, int? cCol = null) { var row = cRow == null ? currentRow : cRow; var col = cCol == null ? currentColumn : cCol; if (cRow == null && cCol == null && wordWrap) { row = wrapManager.GetModelLineFromWrappedLines (currentRow); col = wrapManager.GetModelColFromWrappedLines (currentRow, currentColumn); } UnwrappedCursorPosition?.Invoke (new Point ((int)col, (int)row)); } ustring GetSelectedRegion () { var cRow = currentRow; var cCol = currentColumn; var startRow = selectionStartRow; var startCol = selectionStartColumn; var model = this.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 (startRow, startCol, cRow, cCol, model); } /// public override void Redraw (Rect bounds) { SetNormalColor (); 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)) { SetSelectionColor (line, idxCol); } else if (idxCol == currentColumn && idxRow == currentRow && !selecting && !Used && HasFocus && idxCol < lineRuneCount) { SetSelectionColor (line, idxCol); } else if (ReadOnly) { SetReadOnlyColor (line, idxCol); } else { SetNormalColor (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) { SetNormalColor (); ClearRegion (col, row, right, row + 1); } row++; } if (row < bottom) { SetNormalColor (); ClearRegion (bounds.Left, row, right, bottom); } PositionCursor (); if (clickWithSelecting) { clickWithSelecting = false; return; } if (SelectedLength > 0) return; // draw autocomplete Autocomplete.GenerateSuggestions (); var renderAt = new Point ( CursorPosition.X - LeftColumn, Autocomplete.PopupInsideContainer ? (CursorPosition.Y + 1) - TopRow : 0); Autocomplete.RenderOverlay (renderAt); } /// public override Attribute GetNormalColor () { return Enabled ? ColorScheme.Focus : ColorScheme.Disabled; } /// 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; } /// /// Inserts the given text at the current cursor position /// exactly as if the user had just typed it /// /// Text to add public void InsertText (string toAdd) { foreach (var ch in toAdd) { Key key; try { key = (Key)ch; } catch (Exception) { throw new ArgumentException ($"Cannot insert character '{ch}' because it does not map to a Key"); } InsertText (new KeyEvent () { Key = key }); } if (NeedDisplay.IsEmpty) { PositionCursor (); } else { Adjust (); } } 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); } var prow = currentRow - topRow; if (!wrapNeeded) { SetNeedsDisplay (new Rect (0, prow, Math.Max (Frame.Width, 0), Math.Max (prow + 1, 0))); } } 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); } /// /// Returns the characters on the current line (where the cursor is positioned). /// Use to determine the position of the cursor within /// that line /// /// public List GetCurrentLine () => model.GetLine (currentRow); void InsertText (ustring text) { if (ustring.IsNullOrEmpty (text)) { return; } var lines = TextModel.StringToRunes (text); if (lines.Count == 0) { return; } SetWrapModel (); var line = GetCurrentLine (); historyText.Add (new List> () { new List (line) }, CursorPosition); // Optimize single line if (lines.Count == 1) { line.InsertRange (currentColumn, lines [0]); currentColumn += lines [0].Count; historyText.Add (new List> () { new List (line) }, CursorPosition, HistoryText.LineStatus.Replaced); 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))); } UpdateWrapModel (); OnContentsChanged (); return; } List rest = null; int lastp = 0; if (model.Count > 0 && line.Count > 0 && !copyWithoutSelection) { // 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]); var addedLines = new List> () { new List (line) }; for (int i = 1; i < lines.Count; i++) { model.AddLine (currentRow + i, lines [i]); addedLines.Add (new List (lines [i])); } if (rest != null) { var last = model.GetLine (currentRow + lines.Count - 1); lastp = last.Count; last.InsertRange (last.Count, rest); addedLines.Last ().InsertRange (addedLines.Last ().Count, rest); } historyText.Add (addedLines, CursorPosition, HistoryText.LineStatus.Added); // Now adjust column and row positions currentRow += lines.Count - 1; currentColumn = rest != null ? lastp : lines [lines.Count - 1].Count; Adjust (); historyText.Add (new List> () { new List (line) }, CursorPosition, HistoryText.LineStatus.Replaced); UpdateWrapModel (); OnContentsChanged (); } // 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 ((wordWrap && leftColumn > 0) || (dSize.size + RightOffset < Frame.Width + offB.width && tSize.size + RightOffset < Frame.Width + offB.width)) { if (leftColumn > 0) { leftColumn = 0; need = true; } } 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; } else if (topRow > 0 && currentRow < topRow) { topRow = Math.Max (topRow - 1, 0); need = true; } if (need) { if (wrapNeeded) { WrapTextModel (); wrapNeeded = false; } SetNeedsDisplay (); } else { PositionCursor (); } OnUnwrappedCursorPosition (); } int AdjustOffset (int valueOffset, bool isRow = true) { var curWrap = isRow ? false : wordWrap; var curLength = isRow ? Lines - 1 : GetCurrentLine ().Count; var curStart = isRow ? topRow : leftColumn; var curOffset = isRow ? bottomOffset : rightOffset; var curSize = isRow ? Frame.Height - valueOffset : Frame.Width - valueOffset; var newStart = curStart; if (!curWrap) { if (curStart > 0 && curOffset > 0 && valueOffset == 0) { newStart = Math.Max (curStart - curOffset, 0); } else if (curStart > 0 && curOffset == 0 && valueOffset > 0) { newStart = Math.Max (Math.Min (curStart + valueOffset, curLength - curSize + 1), 0); } if (newStart != curStart) { Application.MainLoop.Invoke (() => SetNeedsDisplay ()); } } return newStart; } /// /// Event arguments for events for when the contents of the TextView change. E.g. the event. /// public class ContentsChangedEventArgs : EventArgs { /// /// Creates a new instance. /// /// Contains the row where the change occurred. /// Contains the column where the change occured. public ContentsChangedEventArgs (int currentRow, int currentColumn) { Row = currentRow; Col = currentColumn; } /// /// /// Contains the row where the change occurred. /// public int Row { get; private set; } /// /// Contains the column where the change occurred. /// public int Col { get; private set; } } /// /// Called when the contents of the TextView change. E.g. when the user types text or deletes text. Raises /// the event. /// public virtual void OnContentsChanged () { ContentsChanged?.Invoke (new ContentsChangedEventArgs (CurrentRow, CurrentColumn)); } (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 (!wordWrap && idx > maxlength - 1 ? maxlength - 1 : idx, 0); } SetNeedsDisplay (); } bool lastWasKill; bool wrapNeeded; bool shiftSelecting; /// public override bool ProcessKey (KeyEvent kb) { if (!CanFocus) { return true; } // Give autocomplete first opportunity to respond to key presses if (SelectedLength == 0 && Autocomplete.ProcessKey (kb)) { return true; } var result = InvokeKeybindings (new KeyEvent (ShortcutHelper.GetModifiersKey (kb), new KeyModifiers () { Alt = kb.IsAlt, Ctrl = kb.IsCtrl, Shift = kb.IsShift })); if (result != null) return (bool)result; ResetColumnTrack (); // Ignore control characters and other special keys if (kb.Key < Key.Space || kb.Key > Key.CharMask) return false; InsertText (kb); DoNeededAction (); return true; } void RedoChanges () { if (ReadOnly) return; historyText.Redo (); } void UndoChanges () { if (ReadOnly) return; historyText.Undo (); } bool ProcessMovePreviousView () { ResetColumnTrack (); return MovePreviousView (); } bool ProcessMoveNextView () { ResetColumnTrack (); return MoveNextView (); } void ProcessSetOverwrite () { ResetColumnTrack (); SetOverwrite (!Used); } void ProcessSelectAll () { ResetColumnTrack (); SelectAll (); } void MoveTopHomeExtend () { ResetColumnTrack (); StartSelecting (); MoveHome (); } void MoveTopHome () { ResetAllTrack (); if (shiftSelecting && selecting) { StopSelecting (); } MoveHome (); } void MoveBottomEndExtend () { ResetAllTrack (); StartSelecting (); MoveEnd (); } void MoveBottomEnd () { ResetAllTrack (); if (shiftSelecting && selecting) { StopSelecting (); } MoveEnd (); } void ProcessKillWordBackward () { ResetColumnTrack (); KillWordBackward (); } void ProcessKillWordForward () { ResetColumnTrack (); KillWordForward (); } void ProcessMoveWordForwardExtend () { ResetAllTrack (); StartSelecting (); MoveWordForward (); } void ProcessMoveWordForward () { ResetAllTrack (); if (shiftSelecting && selecting) { StopSelecting (); } MoveWordForward (); } void ProcessMoveWordBackwardExtend () { ResetAllTrack (); StartSelecting (); MoveWordBackward (); } void ProcessMoveWordBackward () { ResetAllTrack (); if (shiftSelecting && selecting) { StopSelecting (); } MoveWordBackward (); } void ProcessCut () { ResetColumnTrack (); Cut (); } void ProcessCopy () { ResetColumnTrack (); Copy (); } void ToggleSelecting () { ResetColumnTrack (); selecting = !selecting; selectionStartColumn = currentColumn; selectionStartRow = currentRow; } void ProcessPaste () { ResetColumnTrack (); if (isReadOnly) return; Paste (); } void ProcessMoveEndOfLineExtend () { ResetAllTrack (); StartSelecting (); MoveEndOfLine (); } void ProcessMoveEndOfLine () { ResetAllTrack (); if (shiftSelecting && selecting) { StopSelecting (); } MoveEndOfLine (); } void ProcessDeleteCharRight () { ResetColumnTrack (); DeleteCharRight (); } void ProcessMoveStartOfLineExtend () { ResetAllTrack (); StartSelecting (); MoveStartOfLine (); } void ProcessMoveStartOfLine () { ResetAllTrack (); if (shiftSelecting && selecting) { StopSelecting (); } MoveStartOfLine (); } void ProcessDeleteCharLeft () { ResetColumnTrack (); DeleteCharLeft (); } void ProcessMoveLeftExtend () { ResetAllTrack (); StartSelecting (); MoveLeft (); } 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) { // do not respond (this lets the key press fall through to navigation system - which usually changes focus backward) return false; } ResetAllTrack (); if (shiftSelecting && selecting) { StopSelecting (); } MoveLeft (); return true; } void ProcessMoveRightExtend () { ResetAllTrack (); StartSelecting (); MoveRight (); } bool ProcessMoveRight () { // if the user presses Right (without any control keys) // 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; } ResetAllTrack (); if (shiftSelecting && selecting) { StopSelecting (); } MoveRight (); return true; } void ProcessMoveUpExtend () { ResetColumnTrack (); StartSelecting (); MoveUp (); } void ProcessMoveUp () { ResetContinuousFindTrack (); if (shiftSelecting && selecting) { StopSelecting (); } MoveUp (); } void ProcessMoveDownExtend () { ResetColumnTrack (); StartSelecting (); MoveDown (); } void ProcessMoveDown () { ResetContinuousFindTrack (); if (shiftSelecting && selecting) { StopSelecting (); } MoveDown (); } void ProcessPageUpExtend () { ResetColumnTrack (); StartSelecting (); MovePageUp (); } void ProcessPageUp () { ResetColumnTrack (); if (shiftSelecting && selecting) { StopSelecting (); } MovePageUp (); } void ProcessPageDownExtend () { ResetColumnTrack (); StartSelecting (); MovePageDown (); } void ProcessPageDown () { ResetColumnTrack (); if (shiftSelecting && selecting) { StopSelecting (); } MovePageDown (); } bool MovePreviousView () { if (Application.MdiTop != null) { return SuperView?.FocusPrev () == true; } return false; } bool MoveNextView () { if (Application.MdiTop != null) { return SuperView?.FocusNext () == true; } return false; } bool ProcessBackTab () { ResetColumnTrack (); if (!AllowsTab || isReadOnly) { return ProcessMovePreviousView (); } if (currentColumn > 0) { SetWrapModel (); var currentLine = GetCurrentLine (); if (currentLine.Count > 0 && currentLine [currentColumn - 1] == '\t') { historyText.Add (new List> () { new List (currentLine) }, CursorPosition); currentLine.RemoveAt (currentColumn - 1); currentColumn--; historyText.Add (new List> () { new List (GetCurrentLine ()) }, CursorPosition, HistoryText.LineStatus.Replaced); } UpdateWrapModel (); } DoNeededAction (); return true; } bool ProcessTab () { ResetColumnTrack (); if (!AllowsTab || isReadOnly) { return ProcessMoveNextView (); } InsertText (new KeyEvent ((Key)'\t', null)); DoNeededAction (); return true; } void SetOverwrite (bool overwrite) { Used = overwrite; SetNeedsDisplay (); DoNeededAction (); } bool ProcessReturn () { ResetColumnTrack (); if (!AllowsReturn || isReadOnly) { return false; } SetWrapModel (); var currentLine = GetCurrentLine (); historyText.Add (new List> () { new List (currentLine) }, CursorPosition); if (selecting) { ClearSelectedRegion (); currentLine = GetCurrentLine (); } var restCount = currentLine.Count - currentColumn; var rest = currentLine.GetRange (currentColumn, restCount); currentLine.RemoveRange (currentColumn, restCount); var addedLines = new List> () { new List (currentLine) }; model.AddLine (currentRow + 1, rest); addedLines.Add (new List (model.GetLine (currentRow + 1))); historyText.Add (addedLines, CursorPosition, HistoryText.LineStatus.Added); currentRow++; bool fullNeedsDisplay = false; if (currentRow >= topRow + Frame.Height) { topRow++; fullNeedsDisplay = true; } currentColumn = 0; historyText.Add (new List> () { new List (GetCurrentLine ()) }, CursorPosition, HistoryText.LineStatus.Replaced); if (!wordWrap && currentColumn < leftColumn) { fullNeedsDisplay = true; leftColumn = 0; } if (fullNeedsDisplay) SetNeedsDisplay (); else SetNeedsDisplay (new Rect (0, currentRow - topRow, 2, Frame.Height)); UpdateWrapModel (); DoNeededAction (); OnContentsChanged (); return true; } void KillWordBackward () { if (isReadOnly) return; SetWrapModel (); var currentLine = GetCurrentLine (); historyText.Add (new List> () { new List (GetCurrentLine ()) }, CursorPosition); if (currentColumn == 0) { DeleteTextBackwards (); historyText.ReplaceLast (new List> () { new List (GetCurrentLine ()) }, CursorPosition, HistoryText.LineStatus.Replaced); UpdateWrapModel (); return; } var newPos = WordBackward (currentColumn, currentRow); if (newPos.HasValue && currentRow == newPos.Value.row) { var restCount = currentColumn - newPos.Value.col; currentLine.RemoveRange (newPos.Value.col, restCount); if (wordWrap) { wrapNeeded = true; } currentColumn = newPos.Value.col; } else if (newPos.HasValue) { var restCount = currentLine.Count - currentColumn; currentLine.RemoveRange (currentColumn, restCount); if (wordWrap) { wrapNeeded = true; } currentColumn = newPos.Value.col; currentRow = newPos.Value.row; } historyText.Add (new List> () { new List (GetCurrentLine ()) }, CursorPosition, HistoryText.LineStatus.Replaced); UpdateWrapModel (); if (wrapNeeded) { SetNeedsDisplay (); } else { SetNeedsDisplay (new Rect (0, currentRow - topRow, Frame.Width, Frame.Height)); } DoNeededAction (); } void KillWordForward () { if (isReadOnly) return; SetWrapModel (); var currentLine = GetCurrentLine (); historyText.Add (new List> () { new List (GetCurrentLine ()) }, CursorPosition); if (currentLine.Count == 0 || currentColumn == currentLine.Count) { DeleteTextForwards (); historyText.ReplaceLast (new List> () { new List (GetCurrentLine ()) }, CursorPosition, HistoryText.LineStatus.Replaced); UpdateWrapModel (); return; } var newPos = WordForward (currentColumn, currentRow); 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 (new List> () { new List (GetCurrentLine ()) }, CursorPosition, HistoryText.LineStatus.Replaced); UpdateWrapModel (); if (wrapNeeded) { SetNeedsDisplay (); } else { SetNeedsDisplay (new Rect (0, currentRow - topRow, Frame.Width, Frame.Height)); } DoNeededAction (); } void MoveWordForward () { var newPos = WordForward (currentColumn, currentRow); if (newPos.HasValue) { currentColumn = newPos.Value.col; currentRow = newPos.Value.row; } Adjust (); DoNeededAction (); } void MoveWordBackward () { var newPos = WordBackward (currentColumn, currentRow); if (newPos.HasValue) { currentColumn = newPos.Value.col; currentRow = newPos.Value.row; } Adjust (); DoNeededAction (); } void KillToStartOfLine () { if (isReadOnly) return; if (model.Count == 1 && GetCurrentLine ().Count == 0) { // Prevents from adding line feeds if there is no more lines. return; } SetWrapModel (); var currentLine = GetCurrentLine (); var setLastWasKill = true; if (currentLine.Count > 0 && currentColumn == 0) { UpdateWrapModel (); DeleteTextBackwards (); return; } historyText.Add (new List> () { new List (currentLine) }, CursorPosition); if (currentLine.Count == 0) { if (currentRow > 0) { model.RemoveLine (currentRow); if (model.Count > 0 || lastWasKill) { var val = ustring.Make (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); var removedLine = new List> () { new List (currentLine) }; removedLine.Add (new List ()); historyText.Add (new List> (removedLine), CursorPosition, HistoryText.LineStatus.Removed); currentColumn = currentLine.Count; } } else { var restCount = currentColumn; var rest = currentLine.GetRange (0, restCount); var val = ustring.Empty; val += StringFromRunes (rest); if (lastWasKill) { AppendClipboard (val); } else { SetClipboard (val); } currentLine.RemoveRange (0, restCount); currentColumn = 0; } historyText.Add (new List> () { new List (GetCurrentLine ()) }, CursorPosition, HistoryText.LineStatus.Replaced); UpdateWrapModel (); if (wrapNeeded) SetNeedsDisplay (); else SetNeedsDisplay (new Rect (0, currentRow - topRow, Frame.Width, Frame.Height)); lastWasKill = setLastWasKill; DoNeededAction (); } 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 (); var currentLine = GetCurrentLine (); var setLastWasKill = true; if (currentLine.Count > 0 && currentColumn == currentLine.Count) { UpdateWrapModel (); DeleteTextForwards (); return; } historyText.Add (new List> () { new List (currentLine) }, CursorPosition); if (currentLine.Count == 0) { if (currentRow < model.Count - 1) { var removedLines = new List> () { new List (currentLine) }; model.RemoveLine (currentRow); removedLines.Add (new List (GetCurrentLine ())); historyText.Add (new List> (removedLines), CursorPosition, HistoryText.LineStatus.Removed); } if (model.Count > 0 || lastWasKill) { var val = ustring.Make (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 { var restCount = currentLine.Count - currentColumn; var rest = currentLine.GetRange (currentColumn, restCount); var val = ustring.Empty; val += StringFromRunes (rest); if (lastWasKill) { AppendClipboard (val); } else { SetClipboard (val); } currentLine.RemoveRange (currentColumn, restCount); } historyText.Add (new List> () { new List (GetCurrentLine ()) }, CursorPosition, HistoryText.LineStatus.Replaced); UpdateWrapModel (); if (wrapNeeded) SetNeedsDisplay (); else SetNeedsDisplay (new Rect (0, currentRow - topRow, Frame.Width, Frame.Height)); lastWasKill = setLastWasKill; DoNeededAction (); } void MoveEndOfLine () { var currentLine = GetCurrentLine (); currentColumn = currentLine.Count; Adjust (); DoNeededAction (); } void MoveStartOfLine () { currentColumn = 0; leftColumn = 0; Adjust (); DoNeededAction (); } /// /// Deletes all the selected or a single character at right from the position of the cursor. /// public void DeleteCharRight () { if (isReadOnly) return; SetWrapModel (); if (selecting) { historyText.Add (new List> () { new List (GetCurrentLine ()) }, CursorPosition, HistoryText.LineStatus.Original); ClearSelectedRegion (); var currentLine = GetCurrentLine (); historyText.Add (new List> () { new List (currentLine) }, CursorPosition, HistoryText.LineStatus.Replaced); UpdateWrapModel (); return; } if (DeleteTextForwards ()) { UpdateWrapModel (); OnContentsChanged (); return; } UpdateWrapModel (); DoNeededAction (); OnContentsChanged (); } /// /// Deletes all the selected or a single character at left from the position of the cursor. /// public void DeleteCharLeft () { if (isReadOnly) return; SetWrapModel (); if (selecting) { historyText.Add (new List> () { new List (GetCurrentLine ()) }, CursorPosition, HistoryText.LineStatus.Original); ClearSelectedRegion (); var currentLine = GetCurrentLine (); historyText.Add (new List> () { new List (currentLine) }, CursorPosition, HistoryText.LineStatus.Replaced); UpdateWrapModel (); OnContentsChanged (); return; } if (DeleteTextBackwards ()) { UpdateWrapModel (); OnContentsChanged (); return; } UpdateWrapModel (); DoNeededAction (); OnContentsChanged (); } void MoveLeft () { if (currentColumn > 0) { currentColumn--; } else { if (currentRow > 0) { currentRow--; if (currentRow < topRow) { topRow--; SetNeedsDisplay (); } var currentLine = GetCurrentLine (); currentColumn = currentLine.Count; } } Adjust (); DoNeededAction (); } void MoveRight () { 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 (); DoNeededAction (); } void MovePageUp () { 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 (); } DoNeededAction (); } void MovePageDown () { 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 (); } DoNeededAction (); } void ResetContinuousFindTrack () { // Handle some state here - whether the last command was a kill // operation and the column tracking (up/down) lastWasKill = false; continuousFind = false; } void ResetColumnTrack () { // Handle some state here - whether the last command was a kill // operation and the column tracking (up/down) lastWasKill = false; columnTrack = -1; } 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; } bool InsertText (KeyEvent kb) { //So that special keys like tab can be processed if (isReadOnly) return true; SetWrapModel (); historyText.Add (new List> () { new List (GetCurrentLine ()) }, CursorPosition); if (selecting) { ClearSelectedRegion (); } if (kb.Key == Key.Enter) { model.AddLine (currentRow + 1, new List ()); currentRow++; currentColumn = 0; } else if ((uint)kb.Key == 13) { currentColumn = 0; } else { if (Used) { Insert ((uint)kb.Key); currentColumn++; if (currentColumn >= leftColumn + Frame.Width) { leftColumn++; SetNeedsDisplay (); } } else { Insert ((uint)kb.Key); currentColumn++; } } historyText.Add (new List> () { new List (GetCurrentLine ()) }, CursorPosition, HistoryText.LineStatus.Replaced); UpdateWrapModel (); OnContentsChanged (); return true; } void ShowContextMenu () { if (currentCulture != Thread.CurrentThread.CurrentUICulture) { currentCulture = Thread.CurrentThread.CurrentUICulture; ContextMenu.MenuItems = BuildContextMenuBarItem (); } ContextMenu.Show (); } /// /// Deletes all text. /// public void DeleteAll () { if (Lines == 0) { return; } selectionStartColumn = 0; selectionStartRow = 0; MoveBottomEndExtend (); DeleteCharLeft (); SetNeedsDisplay (); } /// 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 () { SetWrapModel (); var currentLine = GetCurrentLine (); if (currentColumn == currentLine.Count) { if (currentRow + 1 == model.Count) { UpdateWrapModel (); return true; } historyText.Add (new List> () { new List (currentLine) }, CursorPosition); var removedLines = new List> () { new List (currentLine) }; var nextLine = model.GetLine (currentRow + 1); removedLines.Add (new List (nextLine)); historyText.Add (removedLines, CursorPosition, HistoryText.LineStatus.Removed); currentLine.AddRange (nextLine); model.RemoveLine (currentRow + 1); historyText.Add (new List> () { new List (currentLine) }, CursorPosition, HistoryText.LineStatus.Replaced); if (wordWrap) { wrapNeeded = true; } if (wrapNeeded) { SetNeedsDisplay (); } else { var sr = currentRow - topRow; SetNeedsDisplay (new Rect (0, sr, Frame.Width, sr + 1)); } } else { historyText.Add (new List> () { new List (currentLine) }, CursorPosition); currentLine.RemoveAt (currentColumn); historyText.Add (new List> () { new List (currentLine) }, CursorPosition, HistoryText.LineStatus.Replaced); if (wordWrap) { wrapNeeded = true; } if (wrapNeeded) { SetNeedsDisplay (); } else { var r = currentRow - topRow; SetNeedsDisplay (new Rect (currentColumn - leftColumn, r, Frame.Width, r + 1)); } } UpdateWrapModel (); return false; } bool DeleteTextBackwards () { SetWrapModel (); if (currentColumn > 0) { // Delete backwards var currentLine = GetCurrentLine (); historyText.Add (new List> () { new List (currentLine) }, CursorPosition); currentLine.RemoveAt (currentColumn - 1); if (wordWrap) { wrapNeeded = true; } currentColumn--; historyText.Add (new List> () { new List (currentLine) }, CursorPosition, HistoryText.LineStatus.Replaced); 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); historyText.Add (new List> () { new List (prevRow) }, CursorPosition); List> removedLines = new List> () { new List (prevRow) }; removedLines.Add (new List (GetCurrentLine ())); historyText.Add (removedLines, new Point (currentColumn, prowIdx), HistoryText.LineStatus.Removed); var prevCount = prevRow.Count; model.GetLine (prowIdx).AddRange (GetCurrentLine ()); model.RemoveLine (currentRow); if (wordWrap) { wrapNeeded = true; } currentRow--; historyText.Add (new List> () { GetCurrentLine () }, new Point (currentColumn, prowIdx), HistoryText.LineStatus.Replaced); currentColumn = prevCount; SetNeedsDisplay (); } UpdateWrapModel (); 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 (); historyText.Add (new List> () { new List (GetCurrentLine ()) }, CursorPosition, HistoryText.LineStatus.Replaced); } UpdateWrapModel (); selecting = false; DoNeededAction (); OnContentsChanged (); } /// /// Paste the clipboard contents into the current selected position. /// public void Paste () { if (isReadOnly) { return; } SetWrapModel (); var contents = Clipboard.Contents; if (copyWithoutSelection && contents.FirstOrDefault (x => x == '\n' || x == '\r') == 0) { var runeList = contents == null ? new List () : contents.ToRuneList (); var currentLine = GetCurrentLine (); historyText.Add (new List> () { new List (currentLine) }, CursorPosition); var addedLine = new List> () { new List (currentLine) }; addedLine.Add (runeList); historyText.Add (new List> (addedLine), CursorPosition, HistoryText.LineStatus.Added); model.AddLine (currentRow, runeList); currentRow++; historyText.Add (new List> () { new List (GetCurrentLine ()) }, CursorPosition, HistoryText.LineStatus.Replaced); SetNeedsDisplay (); OnContentsChanged (); } else { if (selecting) { ClearRegion (); } copyWithoutSelection = false; InsertText (contents); if (selecting) { historyText.ReplaceLast (new List> () { new List (GetCurrentLine ()) }, CursorPosition, HistoryText.LineStatus.Original); } SetNeedsDisplay (); } UpdateWrapModel (); selecting = false; DoNeededAction (); } void StartSelecting () { if (shiftSelecting && selecting) { return; } shiftSelecting = true; selecting = true; selectionStartColumn = currentColumn; selectionStartRow = currentRow; } void StopSelecting () { shiftSelecting = false; selecting = false; isButtonShift = 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 (); } DoNeededAction (); } 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 (); } DoNeededAction (); } 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 (); SetNeedsDisplay (); } /// /// 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 (); SetNeedsDisplay (); } 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 && !Rune.IsLetterOrDigit (rune) && !Rune.IsWhiteSpace (line [col - 1])) { col++; } return true; } else if (col + 1 == line.Count) { col++; } 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; } } bool isButtonShift; bool clickWithSelecting; /// 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) && !ev.Flags.HasFlag (MouseFlags.Button1TripleClicked) && !ev.Flags.HasFlag (ContextMenu.MouseFlags)) { return false; } if (!CanFocus) { return true; } if (!HasFocus) { SetFocus (); } continuousFind = false; // Give autocomplete first opportunity to respond to mouse clicks if (SelectedLength == 0 && Autocomplete.MouseEvent (ev, true)) { return true; } if (ev.Flags == MouseFlags.Button1Clicked) { if (shiftSelecting && !isButtonShift) { StopSelecting (); } 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) { isButtonShift = true; StartSelecting (); } ProcessMouseClick (ev, out _); PositionCursor (); lastWasKill = false; columnTrack = currentColumn; } else if (ev.Flags.HasFlag (MouseFlags.Button1Pressed)) { if (shiftSelecting) { clickWithSelecting = true; StopSelecting (); } ProcessMouseClick (ev, out _); PositionCursor (); if (!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; if (currentColumn == line.Count || (currentColumn > 0 && (line [currentColumn - 1] != ' ' || line [currentColumn] == ' '))) { newPos = WordBackward (currentColumn, currentRow); if (newPos.HasValue) { currentColumn = currentRow == newPos.Value.row ? newPos.Value.col : 0; } } if (!selecting) { StartSelecting (); } newPos = WordForward (currentColumn, currentRow); if (newPos != null && newPos.HasValue) { currentColumn = currentRow == newPos.Value.row ? newPos.Value.col : line.Count; } PositionCursor (); lastWasKill = false; columnTrack = currentColumn; } else if (ev.Flags.HasFlag (MouseFlags.Button1TripleClicked)) { if (selecting) { StopSelecting (); } ProcessMouseClick (ev, out List line); currentColumn = 0; if (!selecting) { StartSelecting (); } currentColumn = line.Count; PositionCursor (); lastWasKill = false; columnTrack = currentColumn; } else if (ev.Flags == ContextMenu.MouseFlags) { ContextMenu.Position = new Point (ev.X + 2, ev.Y + 2); ShowContextMenu (); } 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; } /// /// Allows clearing the items updating the original text. /// public void ClearHistoryChanges () { historyText?.Clear (Text); } } /// /// Renders an overlay on another view at a given point that allows selecting /// from a range of 'autocomplete' options. /// An implementation on a TextView. /// public class TextViewAutocomplete : Autocomplete { /// protected override string GetCurrentWord (int columnOffset = 0) { var host = (TextView)HostControl; var currentLine = host.GetCurrentLine (); var cursorPosition = Math.Min (host.CurrentColumn + columnOffset, currentLine.Count); return IdxToWord (currentLine, cursorPosition, columnOffset); } /// protected override void DeleteTextBackwards () { ((TextView)HostControl).DeleteCharLeft (); } /// protected override void InsertText (string accepted) { ((TextView)HostControl).InsertText (accepted); } } }