Browse Source

Fixes #2680. Make the TextView API more extensible. (#2682)

* Fixes #2680. Make the TextView API more extensible.

* Remove unnecessary using.

* Add GetLine method.

* Change RuneCell Attribute property to ColorScheme property.

* Add LoadRuneCells method and unit test.

* Add helper method to set all the Colors.ColorSchemes with the same attribute.

* Change RuneCell to class.

* Add IEquatable<RuneCell> interface.

* Fix unit test.

* Still fixing unit test.

* Fixes #2688. ReadOnly TextView's broken scrolling after version update.

* keyModifiers must be reset after key up was been processed.

* Trying fix server unit test error.

* Prevents throw an exception if RuneCell is null.

* Still trying fix this unit test.

* Cleaning code.

* Fix when the RuneCell is null.

* Fix throwing an exception if current column position is greater than the line length.

* Fixes #2689. Autocomplete doesn't popup after typing the first character.

* Fix Used on TextField.

* Always use the original ColorScheme if RuneCell.ColorScheme is null.

* Fix Used on TextView.

* Add RuneCellEventArgs and draw colors events.

* Add two more samples to the scenario.

* Fix a bug which was causing unit tests with ColorScheme fail.

* Fix a issue when WordWrap is true by always loading the old text.

* Improves debugging in RuneCell.

* WordWrap is now preserving the ColorScheme of the unwrapped lines.

* Simplifying unit test.

* Ensures the foreground and background colors are never the same if Used is false.

* Remove nullable from the parameter.

* Merge syntax highlighting of quotes and keywords together

* Add IdxRow property into the RuneCellEventArgs.

* Fix pos calculation on windows
(where newline in Text is \r\n not \n)

* Fix events not being cleared when toggling samples.

* Change Undo and Redo to a public method.

* Changes some methods names to be more explicit.

* Call OnContentsChanged on needed methods and fix some more bugs.

* Adds InheritsPreviousColorScheme to allow LoadRuneCells uses personalized color schemes.

* Serializes and deserializes RuneCell to a .rce extension file.

* Prevents throwing if column is bigger than the line.

* Avoids create a color attribute without one of the foreground or background values. In Linux using -1 throws an exception.

* Replace SetAllAttributesBasedOn method with a ColorScheme constructor.

* Move RuneCell string extensions to TextView.cs

* Reverted parameter name from cell to rune.

* Change Row to UnwrappedPosition which provide the real unwrapped text position within the Col.

* Add brackets to Undo and Redo methods.

* Replace all the LoadXXX with Load and rely on the param type to differentiate.

* Open a file inside a using.

* Proves that the events run twice for WordWrap disabled and the enabled.

* Remove GetColumns extension for RuneCell.

* Add braces to Undo an Redo.

* Change comment.

* Add braces.

* Delete remarks tag.

* Explaining used color and ProcessInheritsPreviousColorScheme.

* Fix comment.

* Created a RuneCellTests.cs file.

* Rename to StringToLinesOfRuneCells.

* Make ToRuneCells private.

---------

Co-authored-by: Thomas <[email protected]>
Co-authored-by: Thomas Nind <[email protected]>
BDisp 2 years ago
parent
commit
325180ae48

+ 10 - 4
Terminal.Gui/Configuration/AttributeJsonConverter.cs

@@ -30,11 +30,12 @@ namespace Terminal.Gui {
 			}
 
 			Attribute attribute = new Attribute ();
-			Color foreground =  (Color)(-1);
-			Color background =  (Color)(-1);
+			Color foreground = (Color)(-1);
+			Color background = (Color)(-1);
+			int valuePair = 0;
 			while (reader.Read ()) {
 				if (reader.TokenType == JsonTokenType.EndObject) {
-					if (foreground ==  (Color)(-1) || background ==  (Color)(-1)) {
+					if (foreground == (Color)(-1) || background == (Color)(-1)) {
 						throw new JsonException ($"Both Foreground and Background colors must be provided.");
 					}
 					return attribute;
@@ -48,6 +49,8 @@ namespace Terminal.Gui {
 				reader.Read ();
 				string color = $"\"{reader.GetString ()}\"";
 
+				valuePair++;
+
 				switch (propertyName.ToLower ()) {
 				case "foreground":
 					foreground = JsonSerializer.Deserialize<Color> (color, options);
@@ -68,7 +71,10 @@ namespace Terminal.Gui {
 					throw new JsonException ($"Unknown Attribute property {propertyName}.");
 				}
 
-				attribute = new Attribute (foreground, background);
+				if (valuePair == 2) {
+					attribute = new Attribute (foreground, background);
+					valuePair = 0;
+				}
 			}
 			throw new JsonException ();
 		}

+ 9 - 7
Terminal.Gui/Configuration/RuneJsonConverter.cs

@@ -107,13 +107,15 @@ internal class RuneJsonConverter : JsonConverter<Rune> {
 		// HACK: Writes a JSON comment in addition to the glyph to ease debugging.
 		// Technically, JSON comments are not valid, but we use relaxed decoding
 		// (ReadCommentHandling = JsonCommentHandling.Skip)
-		writer.WriteCommentValue ($"(U+{value.Value:X8})");
-		var printable = value.MakePrintable ();
-		if (printable == Rune.ReplacementChar) {
-			writer.WriteStringValue (value.ToString ());
-		} else {
-			writer.WriteRawValue ($"\"{value}\"");
-		}
+		//writer.WriteCommentValue ($"(U+{value.Value:X8})");
+		//var printable = value.MakePrintable ();
+		//if (printable == Rune.ReplacementChar) {
+		//	writer.WriteStringValue (value.ToString ());
+		//} else {
+		//	//writer.WriteRawValue ($"\"{value}\"");
+		//}
+
+		writer.WriteNumberValue (value.Value);
 	}
 }
 #pragma warning restore 1591

+ 16 - 3
Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs

@@ -352,7 +352,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Creates a new instance, initialized with the values from <paramref name="scheme"/>.
 		/// </summary>
-		/// <param name="scheme">The scheme to initlize the new instance with.</param>
+		/// <param name="scheme">The scheme to initialize the new instance with.</param>
 		public ColorScheme (ColorScheme scheme) : base ()
 		{
 			if (scheme != null) {
@@ -364,6 +364,19 @@ namespace Terminal.Gui {
 			}
 		}
 
+		/// <summary>
+		/// Creates a new instance, initialized with the values from <paramref name="attribute"/>.
+		/// </summary>
+		/// <param name="attribute">The attribute to initialize the new instance with.</param>
+		public ColorScheme (Attribute attribute)
+		{
+			_normal = attribute;
+			_focus = attribute;
+			_hotNormal = attribute;
+			_disabled = attribute;
+			_hotFocus = attribute;
+		}
+
 		/// <summary>
 		/// The foreground and background color for text when the view is not focused, hot, or disabled.
 		/// </summary>
@@ -764,13 +777,13 @@ namespace Terminal.Gui {
 			}
 			return true;
 		}
-		
+
 		/// <summary>
 		/// Adds the specified rune to the display at the current cursor position.
 		/// </summary>
 		/// <param name="rune">Rune to add.</param>
 		public abstract void AddRune (Rune rune);
-		
+
 		/// <summary>
 		/// Ensures that the column and line are in a valid range from the size of the driver.
 		/// </summary>

+ 1 - 1
Terminal.Gui/ConsoleDrivers/WindowsDriver.cs

@@ -867,7 +867,7 @@ namespace Terminal.Gui {
 						keyUpHandler (new KeyEvent (map, keyModifiers));
 					}
 				}
-				if (!inputEvent.KeyEvent.bKeyDown && inputEvent.KeyEvent.dwControlKeyState == 0) {
+				if (!inputEvent.KeyEvent.bKeyDown) {
 					keyModifiers = null;
 				}
 				break;

+ 3 - 0
Terminal.Gui/Text/Autocomplete/AutocompleteBase.cs

@@ -48,6 +48,9 @@ namespace Terminal.Gui {
 		/// <inheritdoc/>
 		public virtual Key Reopen { get; set; } = Key.Space | Key.CtrlMask | Key.AltMask;
 
+		/// <inheritdoc/>
+		public virtual AutocompleteContext Context { get; set; }
+
 		/// <inheritdoc/>
 		public abstract bool MouseEvent (MouseEvent me, bool fromHost = false);
 

+ 9 - 4
Terminal.Gui/Text/Autocomplete/AutocompleteContext.cs

@@ -11,7 +11,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// The text on the current line.
 		/// </summary>
-		public List<Rune> CurrentLine { get; set; }
+		public List<RuneCell> CurrentLine { get; set; }
 
 		/// <summary>
 		/// The position of the input cursor within the <see cref="CurrentLine"/>.
@@ -19,13 +19,18 @@ namespace Terminal.Gui {
 		public int CursorPosition { get; set; }
 
 		/// <summary>
-		/// Creates anew instance of the <see cref="AutocompleteContext"/> class
+		/// Gets or sets if the autocomplete was canceled from popup.
 		/// </summary>
-		public AutocompleteContext (List<Rune> currentLine, int cursorPosition)
+		public bool Canceled { get; set; }
+
+		/// <summary>
+		/// Creates a new instance of the <see cref="AutocompleteContext"/> class
+		/// </summary>
+		public AutocompleteContext (List<RuneCell> currentLine, int cursorPosition, bool canceled = false)
 		{
 			CurrentLine = currentLine;
 			CursorPosition = cursorPosition;
+			Canceled = canceled;
 		}
 	}
 }
-

+ 5 - 0
Terminal.Gui/Text/Autocomplete/IAutocomplete.cs

@@ -68,6 +68,11 @@ namespace Terminal.Gui {
 		/// </summary>
 		Key Reopen { get; set; }
 
+		/// <summary>
+		/// The context used by the autocomplete menu.
+		/// </summary>
+		AutocompleteContext Context { get; set; }
+
 		/// <summary>
 		/// Renders the autocomplete dialog inside the given <see cref="HostControl"/> at the
 		/// given point.

+ 15 - 1
Terminal.Gui/Text/Autocomplete/PopupAutocomplete.cs

@@ -151,9 +151,14 @@ namespace Terminal.Gui {
 		/// <param name="renderAt"></param>
 		public override void RenderOverlay (Point renderAt)
 		{
-			if (!Visible || HostControl?.HasFocus == false || Suggestions.Count == 0) {
+			if (!Context.Canceled && Suggestions.Count > 0 && !Visible && HostControl?.HasFocus == true) {
+				ProcessKey (new KeyEvent ((Key)(Suggestions [0].Title [0]), new KeyModifiers ()));
+			} else if (!Visible || HostControl?.HasFocus == false || Suggestions.Count == 0) {
 				LastPopupPos = null;
 				Visible = false;
+				if (Suggestions.Count == 0) {
+					Context.Canceled = false;
+				}
 				return;
 			}
 
@@ -283,6 +288,7 @@ namespace Terminal.Gui {
 			}
 
 			if (kb.Key == Reopen) {
+				Context.Canceled = false;
 				return ReopenSuggestions ();
 			}
 
@@ -322,6 +328,7 @@ namespace Terminal.Gui {
 
 			if (kb.Key == CloseKey) {
 				Close ();
+				Context.Canceled = true;
 				return true;
 			}
 
@@ -436,6 +443,7 @@ namespace Terminal.Gui {
 		/// <returns>True if the insertion was possible otherwise false</returns>
 		protected virtual bool InsertSelection (Suggestion accepted)
 		{
+			SetCursorPosition (Context.CursorPosition + accepted.Remove);
 			// delete the text
 			for (int i = 0; i < accepted.Remove; i++) {
 				DeleteTextBackwards ();
@@ -456,6 +464,12 @@ namespace Terminal.Gui {
 		/// <param name="accepted"></param>
 		protected abstract void InsertText (string accepted);
 
+		/// <summary>
+		/// Set the cursor position in the <see cref="HostControl"/>.
+		/// </summary>
+		/// <param name="column"></param>
+		protected abstract void SetCursorPosition (int column);
+
 		/// <summary>
 		/// Closes the Autocomplete context menu if it is showing and <see cref="IAutocomplete.ClearSuggestions"/>
 		/// </summary>

+ 16 - 13
Terminal.Gui/Text/Autocomplete/SingleWordSuggestionGenerator.cs

@@ -5,7 +5,7 @@ using System.Text;
 
 
 namespace Terminal.Gui {
-	
+
 	/// <summary>
 	/// <see cref="ISuggestionGenerator"/> which suggests from a collection
 	/// of words those that match the <see cref="AutocompleteContext"/>. You
@@ -28,7 +28,9 @@ namespace Terminal.Gui {
 				return Enumerable.Empty<Suggestion> ();
 			}
 
-			var currentWord = IdxToWord (context.CurrentLine, context.CursorPosition);
+			var line = context.CurrentLine.Select (c => c.Rune).ToList ();
+			var currentWord = IdxToWord (line, context.CursorPosition, out int startIdx);
+			context.CursorPosition = startIdx < 1 ? startIdx : Math.Min (startIdx + 1, line.Count);
 
 			if (string.IsNullOrWhiteSpace (currentWord)) {
 				return Enumerable.Empty<Suggestion> ();
@@ -46,7 +48,7 @@ namespace Terminal.Gui {
 		/// Return true if the given symbol should be considered part of a word
 		/// and can be contained in matches. Base behavior is to use <see cref="char.IsLetterOrDigit(char)"/>
 		/// </summary>
-		/// <param name="rune"></param>
+		/// <param name="rune">The rune.</param>
 		/// <returns></returns>
 		public virtual bool IsWordChar (Rune rune)
 		{
@@ -68,37 +70,38 @@ namespace Terminal.Gui {
 		/// </summary>
 		/// <param name="line"></param>
 		/// <param name="idx"></param>
+		/// <param name="startIdx">The start index of the word.</param>
 		/// <param name="columnOffset"></param>
 		/// <returns></returns>
-		protected virtual string IdxToWord (List<Rune> line, int idx, int columnOffset = 0)
+		protected virtual string IdxToWord (List<Rune> line, int idx, out int startIdx, int columnOffset = 0)
 		{
 			StringBuilder sb = new StringBuilder ();
-			var endIdx = idx;
+			startIdx = idx;
 
 			// get the ending word index
-			while (endIdx < line.Count) {
-				if (IsWordChar (line [endIdx])) {
-					endIdx++;
+			while (startIdx < line.Count) {
+				if (IsWordChar (line [startIdx])) {
+					startIdx++;
 				} else {
 					break;
 				}
 			}
 
 			// It isn't a word char then there is no way to autocomplete that word
-			if (endIdx == idx && columnOffset != 0) {
+			if (startIdx == idx && columnOffset != 0) {
 				return null;
 			}
 
 			// we are at the end of a word. Work out what has been typed so far
-			while (endIdx-- > 0) {
-				if (IsWordChar (line [endIdx])) {
-					sb.Insert (0, (char)line [endIdx].Value);
+			while (startIdx-- > 0) {
+				if (IsWordChar (line [startIdx])) {
+					sb.Insert (0, (char)line [startIdx].Value);
 				} else {
 					break;
 				}
 			}
+			startIdx = Math.Max (startIdx, 0);
 			return sb.ToString ();
 		}
 	}
 }
-

+ 1 - 1
Terminal.Gui/Text/RuneExtensions.cs

@@ -42,7 +42,7 @@ public static class RuneExtensions {
 		/* if we arrive here, ucs is not a combining or C0/C1 control character */
 		return 1 + (BiSearch (codePoint, _combiningWideChars, _combiningWideChars.GetLength (0) - 1) != 0 ? 1 : 0);
 	}
-	
+
 	/// <summary>
 	/// Returns <see langword="true"/> if the rune is a combining character.
 	/// </summary>

+ 5 - 3
Terminal.Gui/Text/StringExtensions.cs

@@ -1,4 +1,6 @@
-using System;
+#nullable enable
+
+using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
@@ -20,7 +22,7 @@ public static class StringExtensions {
 	///  The text repeated if <paramref name="n"/> is greater than zero, 
 	///  otherwise <see langword="null"/>.
 	/// </returns>
-	public static string Repeat (this string str, int n)
+	public static string? Repeat (this string str, int n)
 	{
 		if (n <= 0) {
 			return null;
@@ -144,7 +146,7 @@ public static class StringExtensions {
 	/// <param name="bytes">The enumerable byte to convert.</param>
 	/// <param name="encoding">The encoding to be used.</param>
 	/// <returns></returns>
-	public static string ToString (IEnumerable<byte> bytes, Encoding encoding = null)
+	public static string ToString (IEnumerable<byte> bytes, Encoding? encoding = null)
 	{
 		if (encoding == null) {
 			encoding = Encoding.UTF8;

+ 2 - 3
Terminal.Gui/Views/AutocompleteFilepathContext.cs

@@ -11,7 +11,7 @@ namespace Terminal.Gui {
 		public FileDialogState State { get; set; }
 
 		public AutocompleteFilepathContext (string currentLine, int cursorPosition, FileDialogState state)
-			: base (currentLine.EnumerateRunes ().ToList (), cursorPosition)
+			: base (TextModel.ToRuneCellList (currentLine), cursorPosition)
 		{
 			this.State = state;
 		}
@@ -30,7 +30,7 @@ namespace Terminal.Gui {
 				return Enumerable.Empty<Suggestion> ();
 			}
 
-			var path = StringExtensions.ToString (context.CurrentLine);
+			var path = TextModel.ToString (context.CurrentLine);
 			var last = path.LastIndexOfAny (FileDialog.Separators);
 
 			if (string.IsNullOrWhiteSpace (path) || !Path.IsPathRooted (path)) {
@@ -81,6 +81,5 @@ namespace Terminal.Gui {
 
 			return true;
 		}
-
 	}
 }

+ 2 - 2
Terminal.Gui/Views/DateField.cs

@@ -193,7 +193,7 @@ namespace Terminal.Gui {
 
 		bool SetText (Rune key)
 		{
-			var text = TextModel.ToRunes (Text);
+			var text = Text.EnumerateRunes ().ToList ();
 			var newText = text.GetRange (0, CursorPosition);
 			newText.Add (key);
 			if (CursorPosition < fieldLen)
@@ -344,7 +344,7 @@ namespace Terminal.Gui {
 			}
 
 			// BUGBUG: This is a hack, we should be able to just use ((Rune)(uint)kb.Key) directly.
-			if (SetText (TextModel.ToRunes (((Rune)(uint)kb.Key).ToString ()).First ())) {
+			if (SetText (((Rune)(uint)kb.Key).ToString ().EnumerateRunes ().First ())) {
 				IncCursorPosition ();
 			}
 

+ 3 - 3
Terminal.Gui/Views/HistoryTextItem.cs

@@ -6,14 +6,14 @@ using System.Text;
 namespace Terminal.Gui {
 	partial class HistoryText {
 		public class HistoryTextItem : EventArgs {
-			public List<List<Rune>> Lines;
+			public List<List<RuneCell>> Lines;
 			public Point CursorPosition;
 			public LineStatus LineStatus;
 			public bool IsUndoing;
 			public Point FinalCursorPosition;
 			public HistoryTextItem RemovedOnAdded;
 
-			public HistoryTextItem (List<List<Rune>> lines, Point curPos, LineStatus linesStatus)
+			public HistoryTextItem (List<List<RuneCell>> lines, Point curPos, LineStatus linesStatus)
 			{
 				Lines = lines;
 				CursorPosition = curPos;
@@ -22,7 +22,7 @@ namespace Terminal.Gui {
 
 			public HistoryTextItem (HistoryTextItem historyTextItem)
 			{
-				Lines = new List<List<Rune>> (historyTextItem.Lines);
+				Lines = new List<List<RuneCell>> (historyTextItem.Lines);
 				CursorPosition = new Point (historyTextItem.CursorPosition.X, historyTextItem.CursorPosition.Y);
 				LineStatus = historyTextItem.LineStatus;
 			}

+ 37 - 0
Terminal.Gui/Views/RuneCellEventArgs.cs

@@ -0,0 +1,37 @@
+using System.Collections.Generic;
+
+namespace Terminal.Gui {
+	/// <summary>
+	/// Args for events that relate to a specific <see cref="RuneCell"/>.
+	/// </summary>
+	public class RuneCellEventArgs {
+		/// <summary>
+		/// The list of runes the RuneCell is part of.
+		/// </summary>
+		public List<RuneCell> Line { get; }
+
+		/// <summary>
+		/// The index of the RuneCell in the line.
+		/// </summary>
+		public int Col { get; }
+
+		/// <summary>
+		/// The unwrapped row and column index into the text containing the RuneCell. 
+		/// Unwrapped means the text without word wrapping or other visual formatting having been applied.
+		/// </summary>
+		public (int Row, int Col) UnwrappedPosition { get; }
+
+		/// <summary>
+		/// Creates a new instance of the <see cref="RuneCellEventArgs"/> class.
+		/// </summary>
+		/// <param name="line">The line.</param>
+		/// <param name="col">The col index.</param>
+		/// <param name="unwrappedPosition">The unwrapped row and col index.</param>
+		public RuneCellEventArgs (List<RuneCell> line, int col, (int Row, int Col) unwrappedPosition)
+		{
+			Line = line;
+			Col = col;
+			UnwrappedPosition = unwrappedPosition;
+		}
+	}
+}

+ 66 - 25
Terminal.Gui/Views/TextField.cs

@@ -101,7 +101,7 @@ namespace Terminal.Gui {
 			if (text == null)
 				text = "";
 
-			this._text = TextModel.ToRunes (text.Split ("\n") [0]);
+			this._text = text.Split ("\n") [0].EnumerateRunes ().ToList ();
 			_point = text.GetRuneCount ();
 			_first = _point > w + 1 ? _point - w + 1 : 0;
 			CanFocus = true;
@@ -128,8 +128,8 @@ namespace Terminal.Gui {
 			AddCommand (Command.Right, () => { MoveRight (); return true; });
 			AddCommand (Command.CutToEndLine, () => { KillToEnd (); return true; });
 			AddCommand (Command.CutToStartLine, () => { KillToStart (); return true; });
-			AddCommand (Command.Undo, () => { UndoChanges (); return true; });
-			AddCommand (Command.Redo, () => { RedoChanges (); return true; });
+			AddCommand (Command.Undo, () => { Undo (); return true; });
+			AddCommand (Command.Redo, () => { Redo (); return true; });
 			AddCommand (Command.WordLeft, () => { MoveWordLeft (); return true; });
 			AddCommand (Command.WordRight, () => { MoveWordRight (); return true; });
 			AddCommand (Command.KillWordForwards, () => { KillWordForwards (); return true; });
@@ -230,8 +230,8 @@ namespace Terminal.Gui {
 					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)),
+					new MenuItem (Strings.ctxUndo, "", () => Undo (), null, null, GetKeyFromCommand (Command.Undo)),
+					new MenuItem (Strings.ctxRedo, "", () => Redo (), null, null, GetKeyFromCommand (Command.Redo)),
 				});
 		}
 
@@ -245,7 +245,7 @@ namespace Terminal.Gui {
 			if (obj == null)
 				return;
 
-			Text = StringExtensions.ToString (obj?.Lines [obj.CursorPosition.Y]);
+			Text = TextModel.ToString (obj?.Lines [obj.CursorPosition.Y]);
 			CursorPosition = obj.CursorPosition.X;
 			Adjust ();
 		}
@@ -321,17 +321,19 @@ namespace Terminal.Gui {
 					return;
 				}
 				ClearAllSelection ();
-				_text = TextModel.ToRunes (newText.NewText);
+				_text = newText.NewText.EnumerateRunes ().ToList ();
 
 				if (!Secret && !_historyText.IsFromHistory) {
-					_historyText.Add (new List<List<Rune>> () { oldText.ToRuneList () },
+					_historyText.Add (new List<List<RuneCell>> () { TextModel.ToRuneCellList (oldText) },
 						new Point (_point, 0));
-					_historyText.Add (new List<List<Rune>> () { _text }, new Point (_point, 0)
+					_historyText.Add (new List<List<RuneCell>> () { TextModel.ToRuneCells (_text) }, new Point (_point, 0)
 						, HistoryText.LineStatus.Replaced);
 				}
 
 				TextChanged?.Invoke (this, new TextChangedEventArgs (oldText));
 
+				ProcessAutocomplete ();
+
 				if (_point > _text.Count) {
 					_point = Math.Max (TextModel.DisplaySize (_text, 0).size - 1, 0);
 				}
@@ -393,6 +395,8 @@ namespace Terminal.Gui {
 		/// </summary>
 		public override void PositionCursor ()
 		{
+			ProcessAutocomplete ();
+
 			var col = 0;
 			for (int idx = _first < 0 ? 0 : _first; idx < _text.Count; idx++) {
 				if (idx == _point)
@@ -437,9 +441,13 @@ namespace Terminal.Gui {
 			}
 		}
 
+		bool _isDrawing = false;
+
 		///<inheritdoc/>
 		public override void OnDrawContent (Rect contentArea)
 		{
+			_isDrawing = true;
+
 			var selColor = new Attribute (ColorScheme.Focus.Background, ColorScheme.Focus.Foreground);
 			SetSelectedStartSelectedLength ();
 
@@ -485,21 +493,31 @@ namespace Terminal.Gui {
 
 			RenderCaption ();
 
-			if (SelectedLength > 0)
+			ProcessAutocomplete ();
+
+			_isDrawing = false;
+		}
+
+		private void ProcessAutocomplete ()
+		{
+			if (_isDrawing) {
 				return;
+			}
+			if (SelectedLength > 0) {
+				return;
+			}
 
 			// draw autocomplete
 			GenerateSuggestions ();
 
 			var renderAt = new Point (
-				CursorPosition - ScrollOffset, 0);
+				Autocomplete.Context.CursorPosition, 0);
 
 			Autocomplete.RenderOverlay (renderAt);
 		}
 
 		private void RenderCaption ()
 		{
-
 			if (HasFocus || Caption == null || Caption.Length == 0
 				|| Text?.Length > 0) {
 				return;
@@ -520,12 +538,13 @@ namespace Terminal.Gui {
 
 		private void GenerateSuggestions ()
 		{
-			var currentLine = Text.ToRuneList ();
+			var currentLine = TextModel.ToRuneCellList (Text);
 			var cursorPosition = Math.Min (this.CursorPosition, currentLine.Count);
+			Autocomplete.Context = new AutocompleteContext (currentLine, cursorPosition,
+				Autocomplete.Context != null ? Autocomplete.Context.Canceled : false);
 
 			Autocomplete.GenerateSuggestions (
-				new AutocompleteContext (currentLine, cursorPosition)
-				);
+				Autocomplete.Context);
 		}
 
 		/// <inheritdoc/>
@@ -548,15 +567,22 @@ namespace Terminal.Gui {
 				return;
 
 			int offB = OffSetBackground ();
+			bool need = !_needsDisplay.IsEmpty || !Used;
 			if (_point < _first) {
 				_first = _point;
+				need = true;
 			} else if (Frame.Width > 0 && (_first + _point - (Frame.Width + offB) == 0 ||
 				  TextModel.DisplaySize (_text, _first, _point).size >= Frame.Width + offB)) {
 
 				_first = Math.Max (TextModel.CalculateLeftColumn (_text, _first,
 					_point, Frame.Width + offB), 0);
+				need = true;
+			}
+			if (need) {
+				SetNeedsDisplay ();
+			} else {
+				PositionCursor ();
 			}
-			SetNeedsDisplay ();
 		}
 
 		int OffSetBackground ()
@@ -642,7 +668,7 @@ namespace Terminal.Gui {
 
 		void InsertText (KeyEvent kb, bool useOldCursorPos = true)
 		{
-			_historyText.Add (new List<List<Rune>> () { _text }, new Point (_point, 0));
+			_historyText.Add (new List<List<RuneCell>> () { TextModel.ToRuneCells (_text) }, new Point (_point, 0));
 
 			List<Rune> newText = _text;
 			if (_length > 0) {
@@ -652,7 +678,7 @@ namespace Terminal.Gui {
 			if (!useOldCursorPos) {
 				_oldCursorPos = _point;
 			}
-			var kbstr = TextModel.ToRunes (((Rune)(uint)kb.Key).ToString ());
+			var kbstr = ((Rune)(uint)kb.Key).ToString ().EnumerateRunes ();
 			if (Used) {
 				_point++;
 				if (_point == newText.Count + 1) {
@@ -732,10 +758,14 @@ namespace Terminal.Gui {
 			Adjust ();
 		}
 
-		void RedoChanges ()
+		/// <summary>
+		/// Redoes the latest changes.
+		/// </summary>
+		public void Redo ()
 		{
-			if (ReadOnly)
+			if (ReadOnly) {
 				return;
+			}
 
 			_historyText.Redo ();
 
@@ -755,10 +785,14 @@ namespace Terminal.Gui {
 			//Adjust ();
 		}
 
-		void UndoChanges ()
+		/// <summary>
+		/// Undoes the latest changes.
+		/// </summary>
+		public void Undo ()
 		{
-			if (ReadOnly)
+			if (ReadOnly) {
 				return;
+			}
 
 			_historyText.Undo ();
 		}
@@ -891,7 +925,7 @@ namespace Terminal.Gui {
 			if (ReadOnly)
 				return;
 
-			_historyText.Add (new List<List<Rune>> () { _text }, new Point (_point, 0));
+			_historyText.Add (new List<List<RuneCell>> () { TextModel.ToRuneCells (_text) }, new Point (_point, 0));
 
 			if (_length == 0) {
 				if (_point == 0)
@@ -922,7 +956,7 @@ namespace Terminal.Gui {
 			if (ReadOnly)
 				return;
 
-			_historyText.Add (new List<List<Rune>> () { _text }, new Point (_point, 0));
+			_historyText.Add (new List<List<RuneCell>> () { TextModel.ToRuneCells (_text) }, new Point (_point, 0));
 
 			if (_length == 0) {
 				if (_text.Count == 0 || _text.Count == _point)
@@ -1151,8 +1185,9 @@ namespace Terminal.Gui {
 		/// </summary>
 		public void ClearAllSelection ()
 		{
-			if (_selectedStart == -1 && _length == 0 && _selectedText == "")
+			if (_selectedStart == -1 && _length == 0 && string.IsNullOrEmpty (_selectedText)) {
 				return;
+			}
 
 			_selectedStart = -1;
 			_length = 0;
@@ -1333,5 +1368,11 @@ namespace Terminal.Gui {
 		{
 			((TextField)HostControl).InsertText (accepted, false);
 		}
+
+		/// <inheritdoc/>
+		protected override void SetCursorPosition (int column)
+		{
+			((TextField)HostControl).CursorPosition = column;
+		}
 	}
 }

File diff suppressed because it is too large
+ 328 - 120
Terminal.Gui/Views/TextView.cs


+ 2 - 2
Terminal.Gui/Views/TimeField.cs

@@ -169,7 +169,7 @@ namespace Terminal.Gui {
 
 		bool SetText (Rune key)
 		{
-			var text = TextModel.ToRunes (Text);
+			var text = Text.EnumerateRunes ().ToList ();
 			var newText = text.GetRange (0, CursorPosition);
 			newText.Add (key);
 			if (CursorPosition < fieldLen)
@@ -260,7 +260,7 @@ namespace Terminal.Gui {
 			if (ReadOnly)
 				return true;
 
-			if (SetText (TextModel.ToRunes (((Rune)(uint)kb.Key).ToString ()).First ()))
+			if (SetText (((Rune)(uint)kb.Key).ToString ().EnumerateRunes ().First ()))
 				IncCursorPosition ();
 
 			return true;

+ 1 - 1
UICatalog/Scenarios/Editor.cs

@@ -229,7 +229,7 @@ namespace UICatalog.Scenarios {
 		{
 			if (_fileName != null) {
 				// FIXED: BUGBUG: #452 TextView.LoadFile keeps file open and provides no way of closing it
-				_textView.LoadFile (_fileName);
+				_textView.Load (_fileName);
 				//_textView.Text = System.IO.File.ReadAllText (_fileName);
 				_originalText = Encoding.Unicode.GetBytes(_textView.Text);
 				Win.Title = _fileName;

+ 325 - 136
UICatalog/Scenarios/SyntaxHighlighting.cs

@@ -1,8 +1,12 @@
 
 using System;
 using System.Collections.Generic;
+using System.ComponentModel;
+using System.IO;
 using System.Linq;
+using System.Reflection;
 using System.Text;
+using System.Text.Json;
 using System.Text.RegularExpressions;
 using Terminal.Gui;
 using Attribute = Terminal.Gui.Attribute;
@@ -14,34 +18,107 @@ namespace UICatalog.Scenarios {
 	[ScenarioCategory ("TextView")]
 	public class SyntaxHighlighting : Scenario {
 
-		SqlTextView textView;
+		TextView textView;
 		MenuItem miWrap;
+		string path = "RuneCells.rce";
+		private HashSet<string> keywords = new HashSet<string> (StringComparer.CurrentCultureIgnoreCase){
+
+			"select",
+			"distinct",
+			"top",
+			"from",
+			"create",
+			"CIPHER",
+			"CLASS_ORIGIN",
+			"CLIENT",
+			"CLOSE",
+			"COALESCE",
+			"CODE",
+			"COLUMNS",
+			"COLUMN_FORMAT",
+			"COLUMN_NAME",
+			"COMMENT",
+			"COMMIT",
+			"COMPACT",
+			"COMPLETION",
+			"COMPRESSED",
+			"COMPRESSION",
+			"CONCURRENT",
+			"CONNECT",
+			"CONNECTION",
+			"CONSISTENT",
+			"CONSTRAINT_CATALOG",
+			"CONSTRAINT_SCHEMA",
+			"CONSTRAINT_NAME",
+			"CONTAINS",
+			"CONTEXT",
+			"CONTRIBUTORS",
+			"COPY",
+			"CPU",
+			"CURSOR_NAME",
+			"primary",
+			"key",
+			"insert",
+			"alter",
+			"add",
+			"update",
+			"set",
+			"delete",
+			"truncate",
+			"as",
+			"order",
+			"by",
+			"asc",
+			"desc",
+			"between",
+			"where",
+			"and",
+			"or",
+			"not",
+			"limit",
+			"null",
+			"is",
+			"drop",
+			"database",
+			"table",
+			"having",
+			"in",
+			"join",
+			"on",
+			"union",
+			"exists",
+		};
+		private ColorScheme blue;
+		private ColorScheme magenta;
+		private ColorScheme white;
+		private ColorScheme green;
 
 		public override void Setup ()
 		{
 			Win.Title = this.GetName ();
-			Win.Y = 1; // menu
-			Win.Height = Dim.Fill (1); // status bar
-			Application.Top.LayoutSubviews ();
 
 			var menu = new MenuBar (new MenuBarItem [] {
-			new MenuBarItem ("_File", new MenuItem [] {
+			new MenuBarItem ("_TextView", new MenuItem [] {
 				miWrap =  new MenuItem ("_Word Wrap", "", () => WordWrap()){CheckType = MenuItemCheckStyle.Checked},
+				null,
+				new MenuItem ("_Syntax Highlighting", "", () => ApplySyntaxHighlighting()),
+				null,
+				new MenuItem ("_Load Rune Cells", "", () => ApplyLoadRuneCells()),
+				new MenuItem ("_Save Rune Cells", "", () => SaveRuneCells()),
+				null,
 				new MenuItem ("_Quit", "", () => Quit()),
 			})
 			});
 			Application.Top.Add (menu);
 
-			textView = new SqlTextView () {
+			textView = new TextView () {
 				X = 0,
 				Y = 0,
 				Width = Dim.Fill (),
-				Height = Dim.Fill (1),
+				Height = Dim.Fill ()
 			};
 
-			textView.Init ();
-
-			textView.Text = "SELECT TOP 100 * \nfrom\n MyDb.dbo.Biochemistry;";
+			ApplySyntaxHighlighting ();
 
 			Win.Add (textView);
 
@@ -52,6 +129,111 @@ namespace UICatalog.Scenarios {
 			Application.Top.Add (statusBar);
 		}
 
+		private void ApplySyntaxHighlighting ()
+		{
+			ClearAllEvents ();
+
+			green = new ColorScheme (new Attribute (Color.Green, Color.Black));
+			blue = new ColorScheme (new Attribute (Color.Blue, Color.Black));
+			magenta = new ColorScheme (new Attribute (Color.Magenta, Color.Black));
+			white = new ColorScheme (new Attribute (Color.White, Color.Black));
+			textView.ColorScheme = white;
+
+			textView.Text = "/*Query to select:\nLots of data*/\nSELECT TOP 100 * \nfrom\n MyDb.dbo.Biochemistry where TestCode = 'blah';";
+
+			textView.Autocomplete.SuggestionGenerator = new SingleWordSuggestionGenerator () {
+				AllSuggestions = keywords.ToList ()
+			};
+
+			textView.TextChanged += (s, e) => HighlightTextBasedOnKeywords ();
+			textView.DrawContent += (s, e) => HighlightTextBasedOnKeywords ();
+			textView.DrawContentComplete += (s, e) => HighlightTextBasedOnKeywords ();
+		}
+
+		private void ApplyLoadRuneCells ()
+		{
+			ClearAllEvents ();
+
+			List<RuneCell> runeCells = new List<RuneCell> ();
+			foreach (var color in Colors.ColorSchemes) {
+				string csName = color.Key;
+				foreach (var rune in csName.EnumerateRunes ()) {
+					runeCells.Add (new RuneCell { Rune = rune, ColorScheme = color.Value });
+				}
+				runeCells.Add (new RuneCell { Rune = (Rune)'\n', ColorScheme = color.Value });
+			}
+
+			if (File.Exists (path)) {
+				//Reading the file  
+				var cells = ReadFromJsonFile<List<List<RuneCell>>> (path);
+				textView.Load (cells);
+			} else {
+				textView.Load (runeCells);
+			}
+			textView.Autocomplete.SuggestionGenerator = new SingleWordSuggestionGenerator ();
+		}
+
+		private void SaveRuneCells ()
+		{
+			//Writing to file  
+			var cells = textView.GetAllLines ();
+			WriteToJsonFile (path, cells);
+		}
+
+		private void ClearAllEvents ()
+		{
+			textView.ClearEventHandlers ("TextChanged");
+			textView.ClearEventHandlers ("DrawContent");
+			textView.ClearEventHandlers ("DrawContentComplete");
+
+			textView.InheritsPreviousColorScheme = false;
+		}
+
+		private void HighlightTextBasedOnKeywords ()
+		{
+			// Comment blocks, quote blocks etc
+			Dictionary<Rune, ColorScheme> blocks = new Dictionary<Rune, ColorScheme> ();
+
+			var comments = new Regex (@"/\*.*?\*/", RegexOptions.Singleline);
+			var commentMatches = comments.Matches (textView.Text);
+
+			var singleQuote = new Regex (@"'.*?'", RegexOptions.Singleline);
+			var singleQuoteMatches = singleQuote.Matches (textView.Text);
+
+			// Find all keywords (ignoring for now if they are in comments, quotes etc)
+			Regex [] keywordRegexes = keywords.Select (k => new Regex ($@"\b{k}\b", RegexOptions.IgnoreCase)).ToArray ();
+			Match [] keywordMatches = keywordRegexes.SelectMany (r => r.Matches (textView.Text)).ToArray ();
+
+			int pos = 0;
+
+			for (int y = 0; y < textView.Lines; y++) {
+
+				var line = textView.GetLine (y);
+
+				for (int x = 0; x < line.Count; x++) {
+					if (commentMatches.Any (m => ContainsPosition (m, pos))) {
+						line [x].ColorScheme = green;
+					} else if (singleQuoteMatches.Any (m => ContainsPosition (m, pos))) {
+						line [x].ColorScheme = magenta;
+					} else if (keywordMatches.Any (m => ContainsPosition (m, pos))) {
+						line [x].ColorScheme = blue;
+					} else {
+						line [x].ColorScheme = white;
+					}
+
+					pos++;
+				}
+
+				// for the \n or \r\n that exists in Text but not the returned lines
+				pos += Environment.NewLine.Length;
+			}
+		}
+
+		private bool ContainsPosition (Match m, int pos)
+		{
+			return pos >= m.Index && pos < m.Index + m.Length;
+		}
+
 		private void WordWrap ()
 		{
 			miWrap.Checked = !miWrap.Checked;
@@ -63,148 +245,155 @@ namespace UICatalog.Scenarios {
 			Application.RequestStop ();
 		}
 
-		private class SqlTextView : TextView {
-
-			private HashSet<string> keywords = new HashSet<string> (StringComparer.CurrentCultureIgnoreCase);
-			private Attribute blue;
-			private Attribute white;
-			private Attribute magenta;
-
-			public void Init ()
-			{
-				keywords.Add ("select");
-				keywords.Add ("distinct");
-				keywords.Add ("top");
-				keywords.Add ("from");
-				keywords.Add ("create");
-				keywords.Add ("CIPHER");
-				keywords.Add ("CLASS_ORIGIN");
-				keywords.Add ("CLIENT");
-				keywords.Add ("CLOSE");
-				keywords.Add ("COALESCE");
-				keywords.Add ("CODE");
-				keywords.Add ("COLUMNS");
-				keywords.Add ("COLUMN_FORMAT");
-				keywords.Add ("COLUMN_NAME");
-				keywords.Add ("COMMENT");
-				keywords.Add ("COMMIT");
-				keywords.Add ("COMPACT");
-				keywords.Add ("COMPLETION");
-				keywords.Add ("COMPRESSED");
-				keywords.Add ("COMPRESSION");
-				keywords.Add ("CONCURRENT");
-				keywords.Add ("CONNECT");
-				keywords.Add ("CONNECTION");
-				keywords.Add ("CONSISTENT");
-				keywords.Add ("CONSTRAINT_CATALOG");
-				keywords.Add ("CONSTRAINT_SCHEMA");
-				keywords.Add ("CONSTRAINT_NAME");
-				keywords.Add ("CONTAINS");
-				keywords.Add ("CONTEXT");
-				keywords.Add ("CONTRIBUTORS");
-				keywords.Add ("COPY");
-				keywords.Add ("CPU");
-				keywords.Add ("CURSOR_NAME");
-				keywords.Add ("primary");
-				keywords.Add ("key");
-				keywords.Add ("insert");
-				keywords.Add ("alter");
-				keywords.Add ("add");
-				keywords.Add ("update");
-				keywords.Add ("set");
-				keywords.Add ("delete");
-				keywords.Add ("truncate");
-				keywords.Add ("as");
-				keywords.Add ("order");
-				keywords.Add ("by");
-				keywords.Add ("asc");
-				keywords.Add ("desc");
-				keywords.Add ("between");
-				keywords.Add ("where");
-				keywords.Add ("and");
-				keywords.Add ("or");
-				keywords.Add ("not");
-				keywords.Add ("limit");
-				keywords.Add ("null");
-				keywords.Add ("is");
-				keywords.Add ("drop");
-				keywords.Add ("database");
-				keywords.Add ("table");
-				keywords.Add ("having");
-				keywords.Add ("in");
-				keywords.Add ("join");
-				keywords.Add ("on");
-				keywords.Add ("union");
-				keywords.Add ("exists");
-
-				Autocomplete.SuggestionGenerator = new SingleWordSuggestionGenerator () {
-					AllSuggestions = keywords.ToList ()
-				};
-
-				magenta = Driver.MakeAttribute (Color.Magenta, Color.Black);
-				blue = Driver.MakeAttribute (Color.Cyan, Color.Black);
-				white = Driver.MakeAttribute (Color.White, Color.Black);
-			}
-
-			protected override void SetNormalColor ()
-			{
-				Driver.SetAttribute (white);
-			}
-
-			protected override void SetNormalColor (List<Rune> line, int idx)
-			{
-				if (IsInStringLiteral (line, idx)) {
-					Driver.SetAttribute (magenta);
-				} else
-				if (IsKeyword (line, idx)) {
-					Driver.SetAttribute (blue);
-				} else {
-					Driver.SetAttribute (white);
+		private bool IsKeyword (List<Rune> line, int idx)
+		{
+			var word = IdxToWord (line, idx);
+
+			if (string.IsNullOrWhiteSpace (word)) {
+				return false;
+			}
+
+			return keywords.Contains (word, StringComparer.CurrentCultureIgnoreCase);
+		}
+
+		private string IdxToWord (List<Rune> line, int idx)
+		{
+			var words = Regex.Split (
+				new string (line.Select (r => (char)r.Value).ToArray ()),
+				"\\b");
+
+			int count = 0;
+			string current = null;
+
+			foreach (var word in words) {
+				current = word;
+				count += word.Length;
+				if (count > idx) {
+					break;
 				}
 			}
 
-			private bool IsInStringLiteral (List<Rune> line, int idx)
-			{
-				string strLine = new string (line.Select (r => (char)r.Value).ToArray ());
+			return current?.Trim ();
+		}
 
-				foreach (Match m in Regex.Matches (strLine, "'[^']*'")) {
-					if (idx >= m.Index && idx < m.Index + m.Length) {
-						return true;
-					}
+		/// <summary>
+		/// Writes the given object instance to a Json file.
+		/// <para>Object type must have a parameterless constructor.</para>
+		/// <para>Only Public properties and variables will be written to the file. These can be any type though, even other classes.</para>
+		/// <para>If there are public properties/variables that you do not want written to the file, decorate them with the [JsonIgnore] attribute.</para>
+		/// </summary>
+		/// <typeparam name="T">The type of object being written to the file.</typeparam>
+		/// <param name="filePath">The file path to write the object instance to.</param>
+		/// <param name="objectToWrite">The object instance to write to the file.</param>
+		/// <param name="append">If false the file will be overwritten if it already exists. If true the contents will be appended to the file.</param>
+		public static void WriteToJsonFile<T> (string filePath, T objectToWrite, bool append = false) where T : new()
+		{
+			TextWriter writer = null;
+			try {
+				var contentsToWriteToFile = JsonSerializer.Serialize (objectToWrite);
+				writer = new StreamWriter (filePath, append);
+				writer.Write (contentsToWriteToFile);
+			} finally {
+				if (writer != null) {
+					writer.Close ();
 				}
+			}
+		}
 
-				return false;
+		/// <summary>
+		/// Reads an object instance from an Json file.
+		/// <para>Object type must have a parameterless constructor.</para>
+		/// </summary>
+		/// <typeparam name="T">The type of object to read from the file.</typeparam>
+		/// <param name="filePath">The file path to read the object instance from.</param>
+		/// <returns>Returns a new instance of the object read from the Json file.</returns>
+		public static T ReadFromJsonFile<T> (string filePath) where T : new()
+		{
+			TextReader reader = null;
+			try {
+				reader = new StreamReader (filePath);
+				var fileContents = reader.ReadToEnd ();
+				return (T)JsonSerializer.Deserialize (fileContents, typeof (T));
+			} finally {
+				if (reader != null) {
+					reader.Close ();
+				}
 			}
+		}
+	}
 
-			private bool IsKeyword (List<Rune> line, int idx)
-			{
-				var word = IdxToWord (line, idx);
+	public static class EventExtensions {
+		public static void ClearEventHandlers (this object obj, string eventName)
+		{
+			if (obj == null) {
+				return;
+			}
+
+			var objType = obj.GetType ();
+			var eventInfo = objType.GetEvent (eventName);
+			if (eventInfo == null) {
+				return;
+			}
+
+			var isEventProperty = false;
+			var type = objType;
+			FieldInfo eventFieldInfo = null;
+			while (type != null) {
+				/* Find events defined as field */
+				eventFieldInfo = type.GetField (eventName, BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
+				if (eventFieldInfo != null && (eventFieldInfo.FieldType == typeof (MulticastDelegate) || eventFieldInfo.FieldType.IsSubclassOf (typeof (MulticastDelegate)))) {
+					break;
+				}
 
-				if (string.IsNullOrWhiteSpace (word)) {
-					return false;
+				/* Find events defined as property { add; remove; } */
+				eventFieldInfo = type.GetField ("EVENT_" + eventName.ToUpper (), BindingFlags.Static | BindingFlags.Instance | BindingFlags.NonPublic);
+				if (eventFieldInfo != null) {
+					isEventProperty = true;
+					break;
 				}
 
-				return keywords.Contains (word, StringComparer.CurrentCultureIgnoreCase);
+				type = type.BaseType;
 			}
 
-			private string IdxToWord (List<Rune> line, int idx)
-			{
-				var words = Regex.Split (
-					new string (line.Select (r => (char)r.Value).ToArray ()),
-					"\\b");
+			if (eventFieldInfo == null) {
+				return;
+			}
 
-				int count = 0;
-				string current = null;
+			if (isEventProperty) {
+				// Default Events Collection Type
+				RemoveHandler<EventHandlerList> (obj, eventFieldInfo);
+				return;
+			}
 
-				foreach (var word in words) {
-					current = word;
-					count += word.Length;
-					if (count > idx) {
-						break;
-					}
-				}
+			if (!(eventFieldInfo.GetValue (obj) is Delegate eventDelegate)) {
+				return;
+			}
+
+			// Remove Field based event handlers
+			foreach (var d in eventDelegate.GetInvocationList ()) {
+				eventInfo.RemoveEventHandler (obj, d);
+			}
+		}
+
+		private static void RemoveHandler<T> (object obj, FieldInfo eventFieldInfo)
+		{
+			var objType = obj.GetType ();
+			var eventPropertyValue = eventFieldInfo.GetValue (obj);
+
+			if (eventPropertyValue == null) {
+				return;
+			}
+
+			var propertyInfo = objType.GetProperties (BindingFlags.NonPublic | BindingFlags.Instance)
+						  .FirstOrDefault (p => p.Name == "Events" && p.PropertyType == typeof (T));
+			if (propertyInfo == null) {
+				return;
+			}
 
-				return current?.Trim ();
+			var eventList = propertyInfo?.GetValue (obj, null);
+			switch (eventList) {
+			case null:
+				return;
 			}
 		}
 	}

+ 8 - 4
UnitTests/Configuration/ThemeTests.cs

@@ -49,16 +49,20 @@ namespace Terminal.Gui.ConfigurationTests {
 			var updatedScheme = Colors.ColorSchemes ["test"];
 			Assert.Equal (Color.Red, updatedScheme.Normal.Foreground);
 			Assert.Equal (Color.Green, updatedScheme.Normal.Background);
+
+			// remove test ColorScheme from Colors to avoid failures on others unit tests with ColorScheme
+			Colors.ColorSchemes.Remove ("test");
+			Assert.Equal (5, Colors.ColorSchemes.Count);
 		}
 
 		[Fact]
 		public void TestApply ()
 		{
 			ConfigurationManager.Reset ();
-			
+
 			var theme = new ThemeScope ();
 			Assert.NotEmpty (theme);
-			
+
 			Themes.Add ("testTheme", theme);
 
 			Assert.Equal (LineStyle.Single, FrameView.DefaultBorderStyle);
@@ -99,9 +103,9 @@ namespace Terminal.Gui.ConfigurationTests {
 			var newTheme = new ThemeScope ();
 			var newColorScheme = new ColorScheme {
 				Normal = new Attribute (Color.Blue, Color.BrightBlue),
-				
+
 				Focus = colorScheme.Focus,
-				HotNormal =colorScheme.HotNormal,
+				HotNormal = colorScheme.HotNormal,
 				HotFocus = colorScheme.HotFocus,
 				Disabled = colorScheme.Disabled,
 			};

+ 63 - 59
UnitTests/Text/AutocompleteTests.cs

@@ -31,8 +31,8 @@ namespace Terminal.Gui.TextTests {
 
 			ac.HostControl = tv;
 			ac.GenerateSuggestions (
-				new AutocompleteContext(
-				tv.Text.ToRuneList(),2));
+				new AutocompleteContext (
+				TextModel.ToRuneCellList (tv.Text), 2));
 
 			Assert.Equal (2, ac.Suggestions.Count);
 			Assert.Equal ("const", ac.Suggestions [0].Title);
@@ -97,46 +97,46 @@ namespace Terminal.Gui.TextTests {
 			Assert.Equal ($"F Fortunately super feature.", tv.Text);
 			Assert.Equal (new Point (1, 0), tv.CursorPosition);
 			Assert.Equal (2, tv.Autocomplete.Suggestions.Count);
-			Assert.Equal ("Fortunately", tv.Autocomplete.Suggestions[0].Replacement);
-			Assert.Equal ("feature", tv.Autocomplete.Suggestions[^1].Replacement);
+			Assert.Equal ("Fortunately", tv.Autocomplete.Suggestions [0].Replacement);
+			Assert.Equal ("feature", tv.Autocomplete.Suggestions [^1].Replacement);
 			Assert.Equal (0, tv.Autocomplete.SelectedIdx);
-			Assert.Equal ("Fortunately", tv.Autocomplete.Suggestions[tv.Autocomplete.SelectedIdx].Replacement);
+			Assert.Equal ("Fortunately", tv.Autocomplete.Suggestions [tv.Autocomplete.SelectedIdx].Replacement);
 			Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ())));
 			top.Draw ();
 			Assert.Equal ($"F Fortunately super feature.", tv.Text);
 			Assert.Equal (new Point (1, 0), tv.CursorPosition);
 			Assert.Equal (2, tv.Autocomplete.Suggestions.Count);
-			Assert.Equal ("Fortunately", tv.Autocomplete.Suggestions[0].Replacement);
-			Assert.Equal ("feature", tv.Autocomplete.Suggestions[^1].Replacement);
+			Assert.Equal ("Fortunately", tv.Autocomplete.Suggestions [0].Replacement);
+			Assert.Equal ("feature", tv.Autocomplete.Suggestions [^1].Replacement);
 			Assert.Equal (1, tv.Autocomplete.SelectedIdx);
-			Assert.Equal ("feature", tv.Autocomplete.Suggestions[tv.Autocomplete.SelectedIdx].Replacement);
+			Assert.Equal ("feature", tv.Autocomplete.Suggestions [tv.Autocomplete.SelectedIdx].Replacement);
 			Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ())));
 			top.Draw ();
 			Assert.Equal ($"F Fortunately super feature.", tv.Text);
 			Assert.Equal (new Point (1, 0), tv.CursorPosition);
 			Assert.Equal (2, tv.Autocomplete.Suggestions.Count);
-			Assert.Equal ("Fortunately", tv.Autocomplete.Suggestions[0].Replacement);
-			Assert.Equal ("feature", tv.Autocomplete.Suggestions[^1].Replacement);
+			Assert.Equal ("Fortunately", tv.Autocomplete.Suggestions [0].Replacement);
+			Assert.Equal ("feature", tv.Autocomplete.Suggestions [^1].Replacement);
 			Assert.Equal (0, tv.Autocomplete.SelectedIdx);
-			Assert.Equal ("Fortunately", tv.Autocomplete.Suggestions[tv.Autocomplete.SelectedIdx].Replacement);
+			Assert.Equal ("Fortunately", tv.Autocomplete.Suggestions [tv.Autocomplete.SelectedIdx].Replacement);
 			Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ())));
 			top.Draw ();
 			Assert.Equal ($"F Fortunately super feature.", tv.Text);
 			Assert.Equal (new Point (1, 0), tv.CursorPosition);
 			Assert.Equal (2, tv.Autocomplete.Suggestions.Count);
-			Assert.Equal ("Fortunately", tv.Autocomplete.Suggestions[0].Replacement);
-			Assert.Equal ("feature", tv.Autocomplete.Suggestions[^1].Replacement);
+			Assert.Equal ("Fortunately", tv.Autocomplete.Suggestions [0].Replacement);
+			Assert.Equal ("feature", tv.Autocomplete.Suggestions [^1].Replacement);
 			Assert.Equal (1, tv.Autocomplete.SelectedIdx);
-			Assert.Equal ("feature", tv.Autocomplete.Suggestions[tv.Autocomplete.SelectedIdx].Replacement);
+			Assert.Equal ("feature", tv.Autocomplete.Suggestions [tv.Autocomplete.SelectedIdx].Replacement);
 			Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ())));
 			top.Draw ();
 			Assert.Equal ($"F Fortunately super feature.", tv.Text);
 			Assert.Equal (new Point (1, 0), tv.CursorPosition);
 			Assert.Equal (2, tv.Autocomplete.Suggestions.Count);
-			Assert.Equal ("Fortunately", tv.Autocomplete.Suggestions[0].Replacement);
-			Assert.Equal ("feature", tv.Autocomplete.Suggestions[^1].Replacement);
+			Assert.Equal ("Fortunately", tv.Autocomplete.Suggestions [0].Replacement);
+			Assert.Equal ("feature", tv.Autocomplete.Suggestions [^1].Replacement);
 			Assert.Equal (0, tv.Autocomplete.SelectedIdx);
-			Assert.Equal ("Fortunately", tv.Autocomplete.Suggestions[tv.Autocomplete.SelectedIdx].Replacement);
+			Assert.Equal ("Fortunately", tv.Autocomplete.Suggestions [tv.Autocomplete.SelectedIdx].Replacement);
 			Assert.True (tv.Autocomplete.Visible);
 			top.Draw ();
 			Assert.True (tv.ProcessKey (new KeyEvent (tv.Autocomplete.CloseKey, new KeyModifiers ())));
@@ -145,25 +145,19 @@ namespace Terminal.Gui.TextTests {
 			Assert.Empty (tv.Autocomplete.Suggestions);
 			Assert.Equal (3, g.AllSuggestions.Count);
 			Assert.False (tv.Autocomplete.Visible);
-			top.Draw ();
+			tv.PositionCursor ();
 			Assert.True (tv.ProcessKey (new KeyEvent (tv.Autocomplete.Reopen, new KeyModifiers ())));
 			Assert.Equal ($"F Fortunately super feature.", tv.Text);
 			Assert.Equal (new Point (1, 0), tv.CursorPosition);
 			Assert.Equal (2, tv.Autocomplete.Suggestions.Count);
 			Assert.Equal (3, g.AllSuggestions.Count);
 			Assert.True (tv.ProcessKey (new KeyEvent (tv.Autocomplete.SelectionKey, new KeyModifiers ())));
-			Assert.Equal ($"Fortunately Fortunately super feature.", tv.Text);
-			Assert.Equal (new Point (11, 0), tv.CursorPosition);
-			Assert.Equal (2, tv.Autocomplete.Suggestions.Count);
-			Assert.Equal ("Fortunately", tv.Autocomplete.Suggestions[0].Replacement);
-			Assert.Equal ("feature", tv.Autocomplete.Suggestions[^1].Replacement);
-			Assert.Equal (0, tv.Autocomplete.SelectedIdx);
-			Assert.Equal ("Fortunately", tv.Autocomplete.Suggestions[tv.Autocomplete.SelectedIdx].Replacement);
-			Assert.True (tv.ProcessKey (new KeyEvent (tv.Autocomplete.CloseKey, new KeyModifiers ())));
+			tv.PositionCursor ();
 			Assert.Equal ($"Fortunately Fortunately super feature.", tv.Text);
 			Assert.Equal (new Point (11, 0), tv.CursorPosition);
 			Assert.Empty (tv.Autocomplete.Suggestions);
 			Assert.Equal (3, g.AllSuggestions.Count);
+			Assert.False (tv.Autocomplete.Visible);
 		}
 
 		[Fact, AutoInitShutdown]
@@ -183,13 +177,19 @@ namespace Terminal.Gui.TextTests {
 			top.Add (tv);
 			Application.Begin (top);
 
-			// BUGBUG: v2 - I broke this test and don't have time to figure out why. @tznind - help!
-//			for (int i = 0; i < 7; i++) {
-//				Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
-//				Application.Refresh ();
-//				TestHelpers.AssertDriverContentsWithFrameAre (@"
-//This a long line and against TextView.", output);
-//			}
+			for (int i = 0; i < 7; i++) {
+				Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
+				Application.Refresh ();
+				if (i < 4 || i > 5) {
+					TestHelpers.AssertDriverContentsWithFrameAre (@"
+This a long line and against TextView.", output);
+				} else {
+					TestHelpers.AssertDriverContentsWithFrameAre (@"
+This a long line and against TextView.
+     and                              
+     against                          ", output);
+				}
+			}
 
 			Assert.True (tv.MouseEvent (new MouseEvent () {
 				X = 6,
@@ -198,19 +198,21 @@ namespace Terminal.Gui.TextTests {
 			}));
 			Application.Refresh ();
 			TestHelpers.AssertDriverContentsWithFrameAre (@"
-This a long line and against TextView.", output);
+This a long line and against TextView.
+     and                              
+     against                          ", output);
 
 			Assert.True (tv.ProcessKey (new KeyEvent (Key.g, new KeyModifiers ())));
 			Application.Refresh ();
 			TestHelpers.AssertDriverContentsWithFrameAre (@"
 This ag long line and against TextView.
-       against                         ", output);
+     against                           ", output);
 
 			Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ())));
 			Application.Refresh ();
 			TestHelpers.AssertDriverContentsWithFrameAre (@"
 This ag long line and against TextView.
-      against                          ", output);
+     against                           ", output);
 
 			Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ())));
 			Application.Refresh ();
@@ -223,29 +225,31 @@ This ag long line and against TextView.
 			TestHelpers.AssertDriverContentsWithFrameAre (@"
 This ag long line and against TextView.", output);
 
-			// BUGBUG: v2 - I broke this test and don't have time to figure out why. @tznind - help!
-			//			for (int i = 0; i < 3; i++) {
-			//				Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
-			//				Application.Refresh ();
-			//				TestHelpers.AssertDriverContentsWithFrameAre (@"
-			//This ag long line and against TextView.", output);
-			//			}
-
-//			Assert.True (tv.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers ())));
-//			Application.Refresh ();
-//			TestHelpers.AssertDriverContentsWithFrameAre (@"
-//This a long line and against TextView.", output);
-
-//			Assert.True (tv.ProcessKey (new KeyEvent (Key.n, new KeyModifiers ())));
-//			Application.Refresh ();
-//			TestHelpers.AssertDriverContentsWithFrameAre (@"
-//This an long line and against TextView.
-//       and                             ", output);
-
-//			Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
-//			Application.Refresh ();
-//			TestHelpers.AssertDriverContentsWithFrameAre (@"
-//This an long line and against TextView.", output);
+			for (int i = 0; i < 3; i++) {
+				Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
+				Application.Refresh ();
+				TestHelpers.AssertDriverContentsWithFrameAre (@"
+This ag long line and against TextView.
+     against                           ", output);
+			}
+
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers ())));
+			Application.Refresh ();
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+This a long line and against TextView.
+     and                              
+     against                          ", output);
+
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.n, new KeyModifiers ())));
+			Application.Refresh ();
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+This an long line and against TextView.
+     and                               ", output);
+
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
+			Application.Refresh ();
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+This an long line and against TextView.", output);
 		}
 	}
 }

+ 13 - 0
UnitTests/Views/AppendAutocompleteTests.cs

@@ -25,11 +25,13 @@ namespace Terminal.Gui.TextTests {
 			generator.AllSuggestions = new List<string> { "fish" };
 
 			tf.Draw ();
+			tf.PositionCursor ();
 			TestHelpers.AssertDriverContentsAre ("", output);
 
 			tf.ProcessKey (new KeyEvent (Key.f, new KeyModifiers ()));
 
 			tf.Draw ();
+			tf.PositionCursor ();
 			TestHelpers.AssertDriverContentsAre ("fish", output);
 			Assert.Equal ("f", tf.Text);
 
@@ -57,6 +59,7 @@ namespace Terminal.Gui.TextTests {
 			generator.AllSuggestions = new List<string> { "FISH" };
 
 			tf.Draw ();
+			tf.PositionCursor ();
 			TestHelpers.AssertDriverContentsAre ("", output);
 			tf.ProcessKey (new KeyEvent (Key.m, new KeyModifiers ()));
 			tf.ProcessKey (new KeyEvent (Key.y, new KeyModifiers ()));
@@ -65,6 +68,7 @@ namespace Terminal.Gui.TextTests {
 
 			// Even though there is no match on case we should still get the suggestion
 			tf.Draw ();
+			tf.PositionCursor ();
 			TestHelpers.AssertDriverContentsAre ("my fISH", output);
 			Assert.Equal ("my f", tf.Text);
 
@@ -83,6 +87,7 @@ namespace Terminal.Gui.TextTests {
 			// f is typed and suggestion is "fish"
 			Application.Driver.SendKeys ('f', ConsoleKey.F, false, false, false);
 			tf.Draw ();
+			tf.PositionCursor ();
 			TestHelpers.AssertDriverContentsAre ("fish", output);
 			Assert.Equal ("f", tf.Text);
 
@@ -110,6 +115,7 @@ namespace Terminal.Gui.TextTests {
 			// f is typed and suggestion is "fish"
 			Application.Driver.SendKeys ('f', ConsoleKey.F, false, false, false);
 			tf.Draw ();
+			tf.PositionCursor ();
 			TestHelpers.AssertDriverContentsAre ("fish", output);
 			Assert.Equal ("f", tf.Text);
 
@@ -140,6 +146,7 @@ namespace Terminal.Gui.TextTests {
 			// f is typed we should only see 'f' up to size of View (10)
 			Application.Driver.SendKeys ('f', ConsoleKey.F, false, false, false);
 			tf.Draw ();
+			tf.PositionCursor ();
 			TestHelpers.AssertDriverContentsAre (expectRender, output);
 			Assert.Equal ("f", tf.Text);
 		}
@@ -154,6 +161,7 @@ namespace Terminal.Gui.TextTests {
 			// f is typed and suggestion is "fish"
 			Application.Driver.SendKeys ('f', ConsoleKey.F, false, false, false);
 			tf.Draw ();
+			tf.PositionCursor ();
 			TestHelpers.AssertDriverContentsAre ("fish", output);
 			Assert.Equal ("f", tf.Text);
 
@@ -161,12 +169,14 @@ namespace Terminal.Gui.TextTests {
 			Application.Driver.SendKeys (' ', cycleKey, false, false, false);
 
 			tf.Draw ();
+			tf.PositionCursor ();
 			TestHelpers.AssertDriverContentsAre ("friend", output);
 			Assert.Equal ("f", tf.Text);
 
 			// Should be able to cycle in circles endlessly
 			Application.Driver.SendKeys (' ', cycleKey, false, false, false);
 			tf.Draw ();
+			tf.PositionCursor ();
 			TestHelpers.AssertDriverContentsAre ("fish", output);
 			Assert.Equal ("f", tf.Text);
 		}
@@ -179,6 +189,7 @@ namespace Terminal.Gui.TextTests {
 			// f is typed and suggestion is "fish"
 			Application.Driver.SendKeys ('f', ConsoleKey.F, false, false, false);
 			tf.Draw ();
+			tf.PositionCursor ();
 			TestHelpers.AssertDriverContentsAre ("fish", output);
 			Assert.Equal ("f", tf.Text);
 
@@ -197,6 +208,7 @@ namespace Terminal.Gui.TextTests {
 			// f is typed and suggestion is "fish"
 			Application.Driver.SendKeys ('f', ConsoleKey.F, false, false, false);
 			tf.Draw ();
+			tf.PositionCursor ();
 			TestHelpers.AssertDriverContentsAre ("fish", output);
 			Assert.Equal ("f", tf.Text);
 
@@ -219,6 +231,7 @@ namespace Terminal.Gui.TextTests {
 			generator.AllSuggestions = suggestions.ToList ();
 
 			tf.Draw ();
+			tf.PositionCursor ();
 			TestHelpers.AssertDriverContentsAre ("", output);
 
 			return tf;

+ 1 - 1
UnitTests/Views/ComboBoxTests.cs

@@ -13,7 +13,7 @@ namespace Terminal.Gui.ViewsTests {
 			this.output = output;
 		}
 
-		[Fact]
+		[Fact, AutoInitShutdown]
 		public void Constructors_Defaults ()
 		{
 			var cb = new ComboBox ();

+ 249 - 0
UnitTests/Views/RuneCellTests.cs

@@ -0,0 +1,249 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Terminal.Gui.ViewsTests {
+	public class RuneCellTests {
+		readonly ITestOutputHelper _output;
+
+		public RuneCellTests (ITestOutputHelper output)
+		{
+			_output = output;
+		}
+
+		[Fact]
+		public void Constructor_Defaults ()
+		{
+			var rc = new RuneCell ();
+			Assert.NotNull (rc);
+			Assert.Equal (0, rc.Rune.Value);
+			Assert.Null (rc.ColorScheme);
+		}
+
+		[Fact]
+		public void Equals_True ()
+		{
+			var rc1 = new RuneCell ();
+			var rc2 = new RuneCell ();
+			Assert.True (rc1.Equals (rc2));
+			Assert.True (rc2.Equals (rc1));
+
+			rc1.Rune = new Rune ('a');
+			rc1.ColorScheme = new ColorScheme ();
+			rc2.Rune = new Rune ('a');
+			rc2.ColorScheme = new ColorScheme ();
+			Assert.True (rc1.Equals (rc2));
+			Assert.True (rc2.Equals (rc1));
+		}
+
+		[Fact]
+		public void Equals_False ()
+		{
+			var rc1 = new RuneCell ();
+			var rc2 = new RuneCell () {
+				Rune = new Rune ('a'),
+				ColorScheme = new ColorScheme () { Normal = new Attribute (Color.Red) }
+			};
+			Assert.False (rc1.Equals (rc2));
+			Assert.False (rc2.Equals (rc1));
+
+			rc1.Rune = new Rune ('a');
+			rc1.ColorScheme = new ColorScheme ();
+			Assert.Equal (rc1.Rune, rc2.Rune);
+			Assert.False (rc1.Equals (rc2));
+			Assert.False (rc2.Equals (rc1));
+		}
+
+		[Fact]
+		public void ToString_Override ()
+		{
+			var rc1 = new RuneCell ();
+			var rc2 = new RuneCell () {
+				Rune = new Rune ('a'),
+				ColorScheme = new ColorScheme () { Normal = new Attribute (Color.Red) }
+			};
+			Assert.Equal ("U+0000 '\0'; null", rc1.ToString ());
+			Assert.Equal ("U+0061 'a'; Normal: Red,Red; Focus: White,Black; HotNormal: White,Black; HotFocus: White,Black; Disabled: White,Black", rc2.ToString ());
+		}
+
+		private TextView CreateTextView ()
+		{
+			return new TextView () { Width = 30, Height = 10 };
+		}
+
+		[Fact, AutoInitShutdown]
+		public void RuneCell_LoadRuneCells_InheritsPreviousColorScheme ()
+		{
+			List<RuneCell> runeCells = new List<RuneCell> ();
+			foreach (var color in Colors.ColorSchemes) {
+				string csName = color.Key;
+				foreach (var rune in csName.EnumerateRunes ()) {
+					runeCells.Add (new RuneCell { Rune = rune, ColorScheme = color.Value });
+				}
+				runeCells.Add (new RuneCell { Rune = (Rune)'\n', ColorScheme = color.Value });
+			}
+
+			var tv = CreateTextView ();
+			tv.Load (runeCells);
+			Application.Top.Add (tv);
+			var rs = Application.Begin (Application.Top);
+			Assert.True (tv.InheritsPreviousColorScheme);
+			var expectedText = @"
+TopLevel
+Base    
+Dialog  
+Menu    
+Error   ";
+			TestHelpers.AssertDriverContentsWithFrameAre (expectedText, _output);
+
+			var attributes = new Attribute [] {
+				// 0
+				Colors.TopLevel.Focus,
+				// 1
+				Colors.Base.Focus,
+				// 2
+				Colors.Dialog.Focus,
+				// 3
+				Colors.Menu.Focus,
+				// 4
+				Colors.Error.Focus
+			};
+			var expectedColor = @"
+0000000000
+1111000000
+2222220000
+3333000000
+4444400000";
+			TestHelpers.AssertDriverColorsAre (expectedColor, attributes);
+
+			tv.WordWrap = true;
+			Application.Refresh ();
+			TestHelpers.AssertDriverContentsWithFrameAre (expectedText, _output);
+			TestHelpers.AssertDriverColorsAre (expectedColor, attributes);
+
+			tv.CursorPosition = new Point (6, 2);
+			tv.SelectionStartColumn = 0;
+			tv.SelectionStartRow = 0;
+			Assert.Equal ($"TopLevel{Environment.NewLine}Base{Environment.NewLine}Dialog", tv.SelectedText);
+			tv.Copy ();
+			tv.Selecting = false;
+			tv.CursorPosition = new Point (2, 4);
+			tv.Paste ();
+			Application.Refresh ();
+			expectedText = @"
+TopLevel  
+Base      
+Dialog    
+Menu      
+ErTopLevel
+Base      
+Dialogror ";
+			TestHelpers.AssertDriverContentsWithFrameAre (expectedText, _output);
+			expectedColor = @"
+0000000000
+1111000000
+2222220000
+3333000000
+4444444444
+4444000000
+4444444440";
+			TestHelpers.AssertDriverColorsAre (expectedColor, attributes);
+
+			tv.Undo ();
+			tv.CursorPosition = new Point (0, 3);
+			tv.SelectionStartColumn = 0;
+			tv.SelectionStartRow = 0;
+			Assert.Equal ($"TopLevel{Environment.NewLine}Base{Environment.NewLine}Dialog{Environment.NewLine}", tv.SelectedText);
+			tv.Copy ();
+			tv.Selecting = false;
+			tv.CursorPosition = new Point (2, 4);
+			tv.Paste ();
+			Application.Refresh ();
+			expectedText = @"
+TopLevel  
+Base      
+Dialog    
+Menu      
+ErTopLevel
+Base      
+Dialog    
+ror       ";
+			TestHelpers.AssertDriverContentsWithFrameAre (expectedText, _output);
+			expectedColor = @"
+0000000000
+1111000000
+2222220000
+3333000000
+4444444444
+4444000000
+4444440000
+4440000000";
+			TestHelpers.AssertDriverColorsAre (expectedColor, attributes);
+
+			Application.End (rs);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void RuneCell_LoadRuneCells_Without_ColorScheme_Is_Never_Null ()
+		{
+			var cells = new List<RuneCell> {
+				new RuneCell{Rune = new Rune ('T')},
+				new RuneCell{Rune = new Rune ('e')},
+				new RuneCell{Rune = new Rune ('s')},
+				new RuneCell{Rune = new Rune ('t')}
+			};
+			var tv = CreateTextView ();
+			Application.Top.Add (tv);
+			tv.Load (cells);
+
+			for (int i = 0; i < tv.Lines; i++) {
+				var line = tv.GetLine (i);
+				foreach (var rc in line) {
+					Assert.NotNull (rc.ColorScheme);
+				}
+			}
+		}
+
+		[Fact, AutoInitShutdown]
+		public void RuneCellEventArgs_WordWrap_True ()
+		{
+			var eventCount = 0;
+			var text = new List<List<RuneCell>> () { TextModel.ToRuneCells ("This is the first line.".ToRunes ()), TextModel.ToRuneCells ("This is the second line.".ToRunes ()) };
+			var tv = CreateTextView ();
+			tv.DrawNormalColor += _textView_DrawColor;
+			tv.DrawReadOnlyColor += _textView_DrawColor;
+			tv.DrawSelectionColor += _textView_DrawColor;
+			tv.DrawUsedColor += _textView_DrawColor;
+			void _textView_DrawColor (object sender, RuneCellEventArgs e)
+			{
+				Assert.Equal (e.Line [e.Col], text [e.UnwrappedPosition.Row] [e.UnwrappedPosition.Col]);
+				eventCount++;
+			}
+			tv.Text = $"{TextModel.ToString (text [0])}\n{TextModel.ToString (text [1])}\n";
+			Assert.False (tv.WordWrap);
+			Application.Top.Add (tv);
+			Application.Begin (Application.Top);
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+This is the first line. 
+This is the second line.", _output);
+
+			tv.Width = 10;
+			tv.Height = 25;
+			tv.WordWrap = true;
+			Application.Refresh ();
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+This is
+the    
+first  
+line.  
+This is
+the    
+second 
+line.  ", _output);
+
+			Assert.Equal (eventCount, (text [0].Count + text [1].Count) * 2);
+		}
+	}
+}

+ 67 - 48
UnitTests/Views/TextViewTests.cs

@@ -960,7 +960,7 @@ namespace Terminal.Gui.ViewsTests {
 
 		[Fact]
 		[TextViewTestsAutoInitShutdown]
-		public void Kill_To_End_Delete_Forwards_And_Copy_To_The_Clipboard ()
+		public void Kill_To_End_Delete_Forwards_Copy_To_The_Clipboard_And_Paste ()
 		{
 			_textView.Text = "This is the first line.\nThis is the second line.";
 			var iteration = 0;
@@ -1003,7 +1003,7 @@ namespace Terminal.Gui.ViewsTests {
 
 		[Fact]
 		[TextViewTestsAutoInitShutdown]
-		public void Kill_To_Start_Delete_Backwards_And_Copy_To_The_Clipboard ()
+		public void Kill_To_Start_Delete_Backwards_Copy_To_The_Clipboard_And_Paste ()
 		{
 			_textView.Text = "This is the first line.\nThis is the second line.";
 			_textView.MoveEnd ();
@@ -1917,7 +1917,7 @@ namespace Terminal.Gui.ViewsTests {
 		{
 			var result = false;
 			var tv = new TextView ();
-			Assert.Throws<ArgumentNullException> (() => result = tv.LoadFile (null));
+			Assert.Throws<ArgumentNullException> (() => result = tv.Load ((string)null));
 			Assert.False (result);
 		}
 
@@ -1926,7 +1926,7 @@ namespace Terminal.Gui.ViewsTests {
 		{
 			var result = false;
 			var tv = new TextView ();
-			Assert.Throws<ArgumentException> (() => result = tv.LoadFile (""));
+			Assert.Throws<ArgumentException> (() => result = tv.Load (""));
 			Assert.False (result);
 		}
 
@@ -1935,7 +1935,7 @@ namespace Terminal.Gui.ViewsTests {
 		{
 			var result = false;
 			var tv = new TextView ();
-			Assert.Throws<System.IO.FileNotFoundException> (() => result = tv.LoadFile ("blabla"));
+			Assert.Throws<System.IO.FileNotFoundException> (() => result = tv.Load ("blabla"));
 			Assert.False (result);
 		}
 
@@ -1943,14 +1943,14 @@ namespace Terminal.Gui.ViewsTests {
 		public void LoadStream_Throws_If_Stream_Is_Null ()
 		{
 			var tv = new TextView ();
-			Assert.Throws<ArgumentNullException> (() => tv.LoadStream (null));
+			Assert.Throws<ArgumentNullException> (() => tv.Load ((System.IO.Stream)null));
 		}
 
 		[Fact]
 		public void LoadStream_Stream_Is_Empty ()
 		{
 			var tv = new TextView ();
-			tv.LoadStream (new System.IO.MemoryStream ());
+			tv.Load (new System.IO.MemoryStream ());
 			Assert.Equal ("", tv.Text);
 		}
 
@@ -1959,7 +1959,7 @@ namespace Terminal.Gui.ViewsTests {
 		{
 			var text = "This is the first line.\r\nThis is the second line.\r\n";
 			var tv = new TextView ();
-			tv.LoadStream (new System.IO.MemoryStream (System.Text.Encoding.ASCII.GetBytes (text)));
+			tv.Load (new System.IO.MemoryStream (System.Text.Encoding.ASCII.GetBytes (text)));
 			Assert.Equal ($"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}", tv.Text);
 		}
 
@@ -1968,7 +1968,7 @@ namespace Terminal.Gui.ViewsTests {
 		{
 			var text = "This is the first line.\nThis is the second line.\n";
 			var tv = new TextView ();
-			tv.LoadStream (new System.IO.MemoryStream (System.Text.Encoding.ASCII.GetBytes (text)));
+			tv.Load (new System.IO.MemoryStream (System.Text.Encoding.ASCII.GetBytes (text)));
 			Assert.Equal ($"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}", tv.Text);
 		}
 
@@ -1984,7 +1984,7 @@ namespace Terminal.Gui.ViewsTests {
 				stream.Position = 0;
 
 				var tv = new TextView ();
-				tv.LoadStream (stream);
+				tv.Load (stream);
 
 				Assert.Equal (7, text.Length);
 				Assert.Equal (text.Length, tv.Text.Length);
@@ -2005,7 +2005,7 @@ namespace Terminal.Gui.ViewsTests {
 				stream.Position = 0;
 
 				var tv = new TextView ();
-				tv.LoadStream (stream);
+				tv.Load (stream);
 
 				Assert.Equal (8, text.Length);
 				Assert.Equal (text.Length, tv.Text.Length);
@@ -2197,23 +2197,23 @@ line.
 		public void Internal_Tests ()
 		{
 			var txt = "This is a text.";
-			var txtRunes = TextModel.ToRunes (txt);
+			var txtRunes = TextModel.StringToRuneCells (txt);
 			Assert.Equal (txt.Length, txtRunes.Count);
-			Assert.Equal ('T', txtRunes [0].Value);
-			Assert.Equal ('h', txtRunes [1].Value);
-			Assert.Equal ('i', txtRunes [2].Value);
-			Assert.Equal ('s', txtRunes [3].Value);
-			Assert.Equal (' ', txtRunes [4].Value);
-			Assert.Equal ('i', txtRunes [5].Value);
-			Assert.Equal ('s', txtRunes [6].Value);
-			Assert.Equal (' ', txtRunes [7].Value);
-			Assert.Equal ('a', txtRunes [8].Value);
-			Assert.Equal (' ', txtRunes [9].Value);
-			Assert.Equal ('t', txtRunes [10].Value);
-			Assert.Equal ('e', txtRunes [11].Value);
-			Assert.Equal ('x', txtRunes [12].Value);
-			Assert.Equal ('t', txtRunes [13].Value);
-			Assert.Equal ('.', txtRunes [^1].Value);
+			Assert.Equal ('T', txtRunes [0].Rune.Value);
+			Assert.Equal ('h', txtRunes [1].Rune.Value);
+			Assert.Equal ('i', txtRunes [2].Rune.Value);
+			Assert.Equal ('s', txtRunes [3].Rune.Value);
+			Assert.Equal (' ', txtRunes [4].Rune.Value);
+			Assert.Equal ('i', txtRunes [5].Rune.Value);
+			Assert.Equal ('s', txtRunes [6].Rune.Value);
+			Assert.Equal (' ', txtRunes [7].Rune.Value);
+			Assert.Equal ('a', txtRunes [8].Rune.Value);
+			Assert.Equal (' ', txtRunes [9].Rune.Value);
+			Assert.Equal ('t', txtRunes [10].Rune.Value);
+			Assert.Equal ('e', txtRunes [11].Rune.Value);
+			Assert.Equal ('x', txtRunes [12].Rune.Value);
+			Assert.Equal ('t', txtRunes [13].Rune.Value);
+			Assert.Equal ('.', txtRunes [^1].Rune.Value);
 
 			int col = 0;
 			Assert.True (TextModel.SetCol (ref col, 80, 79));
@@ -2223,11 +2223,11 @@ line.
 			var start = 0;
 			var x = 8;
 			Assert.Equal (8, TextModel.GetColFromX (txtRunes, start, x));
-			Assert.Equal ('a', txtRunes [start + x].Value);
+			Assert.Equal ('a', txtRunes [start + x].Rune.Value);
 			start = 1;
 			x = 7;
 			Assert.Equal (7, TextModel.GetColFromX (txtRunes, start, x));
-			Assert.Equal ('a', txtRunes [start + x].Value);
+			Assert.Equal ('a', txtRunes [start + x].Rune.Value);
 
 			Assert.Equal ((15, 15), TextModel.DisplaySize (txtRunes));
 			Assert.Equal ((6, 6), TextModel.DisplaySize (txtRunes, 1, 7));
@@ -2237,8 +2237,8 @@ line.
 			Assert.Equal (2, TextModel.CalculateLeftColumn (txtRunes, 0, 9, 8));
 
 			var tm = new TextModel ();
-			tm.AddLine (0, TextModel.ToRunes ("This is first line."));
-			tm.AddLine (1, TextModel.ToRunes ("This is last line."));
+			tm.AddLine (0, TextModel.StringToRuneCells ("This is first line."));
+			tm.AddLine (1, TextModel.StringToRuneCells ("This is last line."));
 			Assert.Equal ((new Point (2, 0), true), tm.FindNextText ("is", out bool gaveFullTurn));
 			Assert.False (gaveFullTurn);
 			Assert.Equal ((new Point (5, 0), true), tm.FindNextText ("is", out gaveFullTurn));
@@ -2262,14 +2262,14 @@ line.
 			Assert.True (gaveFullTurn);
 
 			Assert.Equal ((new Point (9, 1), true), tm.ReplaceAllText ("is", false, false, "really"));
-			Assert.Equal (TextModel.ToRunes ("Threally really first line."), tm.GetLine (0));
-			Assert.Equal (TextModel.ToRunes ("Threally really last line."), tm.GetLine (1));
+			Assert.Equal (TextModel.StringToRuneCells ("Threally really first line."), tm.GetLine (0));
+			Assert.Equal (TextModel.StringToRuneCells ("Threally really last line."), tm.GetLine (1));
 			tm = new TextModel ();
-			tm.AddLine (0, TextModel.ToRunes ("This is first line."));
-			tm.AddLine (1, TextModel.ToRunes ("This is last line."));
+			tm.AddLine (0, TextModel.StringToRuneCells ("This is first line."));
+			tm.AddLine (1, TextModel.StringToRuneCells ("This is last line."));
 			Assert.Equal ((new Point (5, 1), true), tm.ReplaceAllText ("is", false, true, "really"));
-			Assert.Equal (TextModel.ToRunes ("This really first line."), tm.GetLine (0));
-			Assert.Equal (TextModel.ToRunes ("This really last line."), tm.GetLine (1));
+			Assert.Equal (TextModel.StringToRuneCells ("This really first line."), tm.GetLine (0));
+			Assert.Equal (TextModel.StringToRuneCells ("This really last line."), tm.GetLine (1));
 		}
 
 		[Fact]
@@ -2600,8 +2600,8 @@ line.
 			Assert.True (tv.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ())));
 			Assert.Equal ($"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text);
 			Assert.Equal (new Point (28, 2), tv.CursorPosition);
-			Assert.Single (tv.Autocomplete.Suggestions);
-			Assert.Equal ("first", tv.Autocomplete.Suggestions [0].Replacement);
+			Assert.Empty (tv.Autocomplete.Suggestions);
+			Assert.False (tv.Autocomplete.Visible);
 			g.AllSuggestions = new List<string> ();
 			tv.Autocomplete.ClearSuggestions ();
 			Assert.Empty (g.AllSuggestions);
@@ -2995,12 +2995,12 @@ line.
 
 			foreach (var ls in Enum.GetValues (typeof (HistoryText.LineStatus))) {
 				if ((HistoryText.LineStatus)ls != HistoryText.LineStatus.Original) {
-					Assert.Throws<ArgumentException> (() => ht.Add (new List<List<Rune>> () { new List<Rune> () }, Point.Empty,
+					Assert.Throws<ArgumentException> (() => ht.Add (new List<List<RuneCell>> () { new List<RuneCell> () }, Point.Empty,
 						(HistoryText.LineStatus)ls));
 				}
 			}
 
-			Assert.Null (Record.Exception (() => ht.Add (new List<List<Rune>> () { new List<Rune> () }, Point.Empty,
+			Assert.Null (Record.Exception (() => ht.Add (new List<List<RuneCell>> () { new List<RuneCell> () }, Point.Empty,
 				HistoryText.LineStatus.Original)));
 		}
 
@@ -6179,7 +6179,7 @@ line.
 			Assert.True (tv.MouseEvent (new MouseEvent () { X = 0, Y = 3, Flags = MouseFlags.Button1Pressed }));
 			tv.Draw ();
 			Assert.Equal (new Point (0, 3), tv.CursorPosition);
-			Assert.Equal (new Point (12, 0), cp);
+			Assert.Equal (new Point (13, 0), cp);
 			TestHelpers.AssertDriverContentsWithFrameAre (@"
 This 
 is   
@@ -6728,12 +6728,12 @@ This is the second line.
 			Kill_Delete_WordBackward ();
 			Assert.Equal (expectedEventCount, eventcount);
 
-			expectedEventCount += 1;
-			Kill_To_End_Delete_Forwards_And_Copy_To_The_Clipboard ();
+			expectedEventCount += 2;
+			Kill_To_End_Delete_Forwards_Copy_To_The_Clipboard_And_Paste ();
 			Assert.Equal (expectedEventCount, eventcount);
 
-			expectedEventCount += 1;
-			Kill_To_Start_Delete_Backwards_And_Copy_To_The_Clipboard ();
+			expectedEventCount += 2;
+			Kill_To_Start_Delete_Backwards_Copy_To_The_Clipboard_And_Paste ();
 			Assert.Equal (expectedEventCount, eventcount);
 		}
 
@@ -6882,7 +6882,7 @@ This is the second line.
 			};
 
 			var text = "This is the first line.\r\nThis is the second line.\r\n";
-			tv.LoadStream (new System.IO.MemoryStream (System.Text.Encoding.ASCII.GetBytes (text)));
+			tv.Load (new System.IO.MemoryStream (System.Text.Encoding.ASCII.GetBytes (text)));
 			Assert.Equal ($"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}", tv.Text);
 
 			Assert.Equal (1, eventcount);
@@ -6906,7 +6906,7 @@ This is the second line.
 			var fileName = "textview.txt";
 			System.IO.File.WriteAllText (fileName, "This is the first line.\r\nThis is the second line.\r\n");
 
-			tv.LoadFile (fileName);
+			tv.Load (fileName);
 			Assert.Equal (1, eventcount);
 			Assert.Equal ($"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}", tv.Text);
 		}
@@ -6958,5 +6958,24 @@ This is the second line.
 			_textView.Paste ();
 			Assert.Equal ("TextView with some more test text. Unicode shouldn't 𝔹Aℝ𝔽!", _textView.Text);
 		}
+
+		[Fact, TextViewTestsAutoInitShutdown]
+		public void WordWrap_True_LoadStream_New_Text ()
+		{
+			Assert.Equal ("TAB to jump between text fields.", _textView.Text);
+			_textView.WordWrap = true;
+			Assert.Equal ("TAB to jump between text fields.", _textView.Text);
+			var text = "This is the first line.\nThis is the second line.\n";
+			using (System.IO.MemoryStream stream = new System.IO.MemoryStream ()) {
+				var writer = new System.IO.StreamWriter (stream);
+				writer.Write (text);
+				writer.Flush ();
+				stream.Position = 0;
+
+				_textView.Load (stream);
+				Assert.Equal ($"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}", _textView.Text);
+				Assert.True (_textView.WordWrap);
+			}
+		}
 	}
 }

Some files were not shown because too many files changed in this diff