Răsfoiți Sursa

Merge branch 'v2_develop' into v2_draw-over-a-modal-view_2478

BDisp 2 ani în urmă
părinte
comite
231194dceb

+ 197 - 0
Terminal.Gui/Core/Autocomplete/AppendAutocomplete.cs

@@ -0,0 +1,197 @@
+using System;
+using System.IO;
+using System.Linq;
+
+namespace Terminal.Gui {
+
+	/// <summary>
+	/// Autocomplete for a <see cref="TextField"/> which shows suggestions within the box.
+	/// Displayed suggestions can be completed using the tab key.
+	/// </summary>
+	public class AppendAutocomplete : AutocompleteBase {
+
+		private TextField textField;
+
+		/// <inheritdoc/>
+		public override View HostControl { get => textField; set => textField = (TextField)value; }
+
+		/// <summary>
+		/// The color used for rendering the appended text. Note that only
+		/// <see cref="ColorScheme.Normal"/> is used and then only <see cref="Attribute.Foreground"/>
+		/// (Background comes from <see cref="HostControl"/>).
+		/// </summary>
+		public override ColorScheme ColorScheme { get; set; }
+
+		/// <summary>
+		///	Creates a new instance of the <see cref="AppendAutocomplete"/> class.
+		/// </summary>
+		public AppendAutocomplete (TextField textField)
+		{
+			this.textField = textField;
+			SelectionKey = Key.Tab;
+
+
+			ColorScheme = new ColorScheme{
+				Normal = new Attribute(Color.DarkGray,0),
+				Focus = new Attribute(Color.DarkGray,0),
+				HotNormal = new Attribute(Color.DarkGray,0),
+				HotFocus = new Attribute(Color.DarkGray,0),
+				Disabled = new Attribute(Color.DarkGray,0),
+			};
+		}
+
+		/// <inheritdoc/>
+		public override void ClearSuggestions ()
+		{
+			base.ClearSuggestions ();
+			textField.SetNeedsDisplay ();
+		}
+
+		/// <inheritdoc/>
+		public override bool MouseEvent (MouseEvent me, bool fromHost = false)
+		{
+			return false;
+		}
+
+		/// <inheritdoc/>
+		public override bool ProcessKey (KeyEvent kb)
+		{
+			var key = kb.Key;
+			if (key == SelectionKey) {
+				return this.AcceptSelectionIfAny ();
+			} else
+			if (key == Key.CursorUp) {
+				return this.CycleSuggestion (1);
+			} else
+			if (key == Key.CursorDown) {
+				return this.CycleSuggestion (-1);
+			}
+			else if(key == CloseKey && Suggestions.Any())
+			{
+				ClearSuggestions();
+				_suspendSuggestions = true;
+				return true;
+			}
+
+			if(char.IsLetterOrDigit((char)kb.KeyValue))
+			{
+				_suspendSuggestions = false;
+			}
+
+			return false;
+		}
+		bool _suspendSuggestions = false;
+
+		/// <inheritdoc/>
+		public override void GenerateSuggestions (AutocompleteContext context)
+		{
+			if(_suspendSuggestions)
+			{
+				return;
+			}
+			base.GenerateSuggestions (context);
+		}
+
+		/// <summary>
+		/// Renders the current suggestion into the <see cref="TextField"/>
+		/// </summary>
+		public override void RenderOverlay (Point renderAt)
+		{
+			if (!this.MakingSuggestion ()) {
+				return;
+			}
+
+			// draw it like its selected even though its not
+			Application.Driver.SetAttribute (new Attribute (ColorScheme.Normal.Foreground, textField.ColorScheme.Focus.Background));
+			textField.Move (textField.Text.Length, 0);
+
+			var suggestion = this.Suggestions.ElementAt (this.SelectedIdx);
+			var fragment = suggestion.Replacement.Substring (suggestion.Remove);
+
+			int spaceAvailable = textField.Bounds.Width - textField.Text.ConsoleWidth;
+			int spaceRequired = fragment.Sum(c=>Rune.ColumnWidth(c));
+
+			if(spaceAvailable < spaceRequired)
+			{
+				fragment = new string(
+					fragment.TakeWhile(c=> (spaceAvailable -= Rune.ColumnWidth(c)) >= 0)
+					.ToArray()
+				);
+			}
+
+			Application.Driver.AddStr (fragment);
+		}
+
+		/// <summary>
+		/// Accepts the current autocomplete suggestion displaying in the text box.
+		/// Returns true if a valid suggestion was being rendered and acceptable or
+		/// false if no suggestion was showing.
+		/// </summary>
+		/// <returns></returns>
+		internal bool AcceptSelectionIfAny ()
+		{
+			if (this.MakingSuggestion ()) {
+
+				var insert = this.Suggestions.ElementAt (this.SelectedIdx);
+				var newText = textField.Text.ToString ();
+				newText = newText.Substring (0, newText.Length - insert.Remove);
+				newText += insert.Replacement;
+				textField.Text = newText;
+
+				this.MoveCursorToEnd ();
+
+				this.ClearSuggestions ();
+				return true;
+			}
+
+			return false;
+		}
+
+		internal void MoveCursorToEnd ()
+		{
+			textField.ClearAllSelection ();
+			textField.CursorPosition = textField.Text.Length;
+		}
+
+		internal void SetTextTo (FileSystemInfo fileSystemInfo)
+		{
+			var newText = fileSystemInfo.FullName;
+			if (fileSystemInfo is DirectoryInfo) {
+				newText += System.IO.Path.DirectorySeparatorChar;
+			}
+			textField.Text = newText;
+			this.MoveCursorToEnd ();
+		}
+
+		internal bool CursorIsAtEnd ()
+		{
+			return textField.CursorPosition == textField.Text.Length;
+		}
+
+		/// <summary>
+		/// Returns true if there is a suggestion that can be made and the control
+		/// is in a state where user would expect to see auto-complete (i.e. focused and
+		/// cursor in right place).
+		/// </summary>
+		/// <returns></returns>
+		private bool MakingSuggestion ()
+		{
+			return Suggestions.Any () && this.SelectedIdx != -1 && textField.HasFocus && this.CursorIsAtEnd ();
+		}
+
+		private bool CycleSuggestion (int direction)
+		{
+			if (this.Suggestions.Count <= 1) {
+				return false;
+			}
+
+			this.SelectedIdx = (this.SelectedIdx + direction) % this.Suggestions.Count;
+
+			if (this.SelectedIdx < 0) {
+				this.SelectedIdx = this.Suggestions.Count () - 1;
+			}
+			textField.SetNeedsDisplay ();
+			return true;
+		}
+	}
+}

+ 79 - 0
Terminal.Gui/Core/Autocomplete/Autocomplete.cd

@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ClassDiagram MajorVersion="1" MinorVersion="1">
+  <Class Name="Terminal.Gui.AppendAutocomplete" Collapsed="true">
+    <Position X="0.5" Y="6.5" Width="2" />
+    <TypeIdentifier>
+      <HashCode>AAAgAABAAQIAAAAAAAAAAAAABAAAIAQAgAEIAggAIAA=</HashCode>
+      <FileName>Core\Autocomplete\AppendAutocomplete.cs</FileName>
+    </TypeIdentifier>
+  </Class>
+  <Class Name="Terminal.Gui.AutocompleteBase" Collapsed="true">
+    <Position X="1.75" Y="5.25" Width="1.5" />
+    <TypeIdentifier>
+      <HashCode>AAQgAAAAAUAAIAAAAAIAAAAAAAEAIAQIgQAIQAAAMBA=</HashCode>
+      <FileName>Core\Autocomplete\AutocompleteBase.cs</FileName>
+    </TypeIdentifier>
+    <Lollipop Position="0.2" />
+  </Class>
+  <Class Name="Terminal.Gui.PopupAutocomplete" Collapsed="true">
+    <Position X="2.75" Y="6.5" Width="1.5" />
+    <NestedTypes>
+      <Class Name="Terminal.Gui.PopupAutocomplete.Popup" Collapsed="true">
+        <TypeIdentifier>
+          <NewMemberFileName>Core\Autocomplete\PopupAutocomplete.cs</NewMemberFileName>
+        </TypeIdentifier>
+      </Class>
+    </NestedTypes>
+    <TypeIdentifier>
+      <HashCode>IAEhAAQAASBEQAAAAAIBAAgYAAAAIAwAwKAAQACBAAA=</HashCode>
+      <FileName>Core\Autocomplete\PopupAutocomplete.cs</FileName>
+    </TypeIdentifier>
+  </Class>
+  <Class Name="Terminal.Gui.SingleWordSuggestionGenerator" BaseTypeListCollapsed="true">
+    <Position X="6.25" Y="3.5" Width="3" />
+    <TypeIdentifier>
+      <HashCode>CEAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAIAA=</HashCode>
+      <FileName>Core\Autocomplete\SingleWordSuggestionGenerator.cs</FileName>
+    </TypeIdentifier>
+    <Lollipop Position="0.2" />
+  </Class>
+  <Class Name="Terminal.Gui.Suggestion">
+    <Position X="4.5" Y="2.5" Width="1.5" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAAAAAAEAAAAAABAAAAAAAAAAAAAAAAAAAAAE=</HashCode>
+      <FileName>Core\Autocomplete\Suggestion.cs</FileName>
+    </TypeIdentifier>
+  </Class>
+  <Class Name="Terminal.Gui.TextFieldAutocomplete" Collapsed="true">
+    <Position X="1.5" Y="7.5" Width="2" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAgAAAAAAAAAA=</HashCode>
+      <FileName>Views\TextField.cs</FileName>
+    </TypeIdentifier>
+  </Class>
+  <Class Name="Terminal.Gui.TextViewAutocomplete" Collapsed="true">
+    <Position X="3.75" Y="7.5" Width="2.25" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAgAAAAAAAAAA=</HashCode>
+      <FileName>Views\TextView.cs</FileName>
+    </TypeIdentifier>
+  </Class>
+  <Interface Name="Terminal.Gui.IAutocomplete">
+    <Position X="1.75" Y="0.5" Width="2.5" />
+    <TypeIdentifier>
+      <HashCode>AAQgAAAAAUAAIAAAAAAAAAAAAAEAIAQIgQAIQAAAMBA=</HashCode>
+      <FileName>Core\Autocomplete\IAutocomplete.cs</FileName>
+    </TypeIdentifier>
+    <ShowAsAssociation>
+      <Property Name="SuggestionGenerator" />
+    </ShowAsAssociation>
+  </Interface>
+  <Interface Name="Terminal.Gui.ISuggestionGenerator">
+    <Position X="6.25" Y="1.75" Width="2.25" />
+    <TypeIdentifier>
+      <HashCode>AEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAA=</HashCode>
+      <FileName>Core\Autocomplete\ISuggestionGenerator.cs</FileName>
+    </TypeIdentifier>
+  </Interface>
+  <Font Name="Segoe UI" Size="9" />
+</ClassDiagram>

+ 86 - 0
Terminal.Gui/Core/Autocomplete/AutocompleteBase.cs

@@ -0,0 +1,86 @@
+using System;
+using System.Collections.ObjectModel;
+using System.Linq;
+
+namespace Terminal.Gui {
+
+	/// <summary>
+	/// Abstract implementation of <see cref="IAutocomplete"/> allows
+	/// for tailoring how autocomplete is rendered/interacted with.
+	/// </summary>
+	public abstract class AutocompleteBase : IAutocomplete {
+
+		/// <inheritdoc/>
+		public abstract View HostControl { get; set; }
+		/// <inheritdoc/>
+		public bool PopupInsideContainer { get; set; }
+
+		/// <inheritdoc/>
+		public ISuggestionGenerator SuggestionGenerator { get; set; } = new SingleWordSuggestionGenerator ();
+
+		/// <inheritdoc/>
+		public virtual int MaxWidth { get; set; } = 10;
+
+		/// <inheritdoc/>
+		public virtual int MaxHeight { get; set; } = 6;
+
+		/// <inheritdoc/>
+
+
+		/// <inheritdoc/>
+		public virtual bool Visible { get; set; }
+
+		/// <inheritdoc/>
+		public virtual ReadOnlyCollection<Suggestion> Suggestions { get; set; } = new ReadOnlyCollection<Suggestion> (new Suggestion [0]);
+
+
+
+		/// <inheritdoc/>
+		public virtual int SelectedIdx { get; set; }
+
+
+		/// <inheritdoc/>
+		public abstract ColorScheme ColorScheme { get; set; }
+
+		/// <inheritdoc/>
+		public virtual Key SelectionKey { get; set; } = Key.Enter;
+
+		/// <inheritdoc/>
+		public virtual Key CloseKey { get; set; } = Key.Esc;
+
+		/// <inheritdoc/>
+		public virtual Key Reopen { get; set; } = Key.Space | Key.CtrlMask | Key.AltMask;
+
+		/// <inheritdoc/>
+		public abstract bool MouseEvent (MouseEvent me, bool fromHost = false);
+
+		/// <inheritdoc/>
+		public abstract bool ProcessKey (KeyEvent kb);
+		/// <inheritdoc/>
+		public abstract void RenderOverlay (Point renderAt);
+
+		/// <inheritdoc/>>
+		public virtual void ClearSuggestions ()
+		{
+			Suggestions = Enumerable.Empty<Suggestion> ().ToList ().AsReadOnly ();
+		}
+
+
+		/// <inheritdoc/>
+		public virtual void GenerateSuggestions (AutocompleteContext context)
+		{
+			Suggestions = SuggestionGenerator.GenerateSuggestions (context).ToList ().AsReadOnly ();
+
+			EnsureSelectedIdxIsValid ();
+		}
+
+		/// <summary>
+		/// Updates <see cref="SelectedIdx"/> to be a valid index within <see cref="Suggestions"/>
+		/// </summary>
+		public virtual void EnsureSelectedIdxIsValid ()
+		{
+			SelectedIdx = Math.Max (0, Math.Min (Suggestions.Count - 1, SelectedIdx));
+		}
+	}
+}
+

+ 31 - 0
Terminal.Gui/Core/Autocomplete/AutocompleteContext.cs

@@ -0,0 +1,31 @@
+using System.Collections.Generic;
+using Rune = System.Rune;
+
+namespace Terminal.Gui {
+	/// <summary>
+	/// Describes the current state of a <see cref="View"/> which
+	/// is proposing autocomplete. Suggestions are based on this state.
+	/// </summary>
+	public class AutocompleteContext
+	{
+		/// <summary>
+		/// The text on the current line.
+		/// </summary>
+		public List<Rune> CurrentLine { get; set; }
+
+		/// <summary>
+		/// The position of the input cursor within the <see cref="CurrentLine"/>.
+		/// </summary>
+		public int CursorPosition { get; set; }
+
+		/// <summary>
+		/// Creates anew instance of the <see cref="AutocompleteContext"/> class
+		/// </summary>
+		public AutocompleteContext (List<Rune> currentLine, int cursorPosition)
+		{
+			CurrentLine = currentLine;
+			CursorPosition = cursorPosition;
+		}
+	}
+}
+

+ 14 - 11
Terminal.Gui/Core/Autocomplete/IAutocomplete.cs

@@ -40,12 +40,7 @@ namespace Terminal.Gui {
 		/// The strings that form the current list of suggestions to render
 		/// based on what the user has typed so far.
 		/// </summary>
-		ReadOnlyCollection<string> Suggestions { get; set; }
-
-		/// <summary>
-		/// The full set of all strings that can be suggested.
-		/// </summary>
-		List<string> AllSuggestions { get; set; }
+		ReadOnlyCollection<Suggestion> Suggestions { get; set; }
 
 		/// <summary>
 		/// The currently selected index into <see cref="Suggestions"/> that the user has highlighted
@@ -53,7 +48,7 @@ namespace Terminal.Gui {
 		int SelectedIdx { get; set; }
 
 		/// <summary>
-		/// The colors to use to render the overlay.  Accessing this property before
+		/// The colors to use to render the overlay. Accessing this property before
 		/// the Application has been initialized will cause an error
 		/// </summary>
 		ColorScheme ColorScheme { get; set; }
@@ -105,11 +100,19 @@ namespace Terminal.Gui {
 		/// </summary>
 		void ClearSuggestions ();
 
+
+		/// <summary>
+		/// Gets or Sets the class responsible for generating <see cref="Suggestions"/>
+		/// based on a given <see cref="AutocompleteContext"/> of the <see cref="HostControl"/>.
+		/// </summary>
+		ISuggestionGenerator SuggestionGenerator { get; set; }
+
+
 		/// <summary>
-		/// Populates <see cref="Suggestions"/> with all strings in <see cref="AllSuggestions"/> that
-		/// match with the current cursor position/text in the <see cref="HostControl"/>.
+		/// Populates <see cref="Suggestions"/> with all <see cref="Suggestion"/> 
+		/// proposed by <see cref="SuggestionGenerator"/> at the given <paramref name="context"/>
+		/// (cursor position)
 		/// </summary>
-		/// <param name="columnOffset">The column offset. Current (zero - default), left (negative), right (positive).</param>
-		void GenerateSuggestions (int columnOffset = 0);
+		void GenerateSuggestions (AutocompleteContext context);
 	}
 }

+ 25 - 0
Terminal.Gui/Core/Autocomplete/ISuggestionGenerator.cs

@@ -0,0 +1,25 @@
+using System.Collections.Generic;
+using Rune = System.Rune;
+
+namespace Terminal.Gui {
+	/// <summary>
+	/// Generates autocomplete <see cref="Suggestion"/> based on a given cursor location within a string
+	/// </summary>
+	public interface ISuggestionGenerator {
+
+		/// <summary>
+		/// Generates autocomplete <see cref="Suggestion"/> based on a given <paramref name="context"/>
+		/// </summary>
+		IEnumerable<Suggestion> GenerateSuggestions (AutocompleteContext context);
+
+
+		/// <summary>
+		/// Returns <see langword="true"/> if <paramref name="rune"/> is a character that
+		/// would continue autocomplete suggesting. Returns <see langword="false"/> if it
+		/// is a 'breaking' character (i.e. terminating current word boundary)
+		/// </summary>
+		bool IsWordChar (Rune rune);
+
+	}
+}
+

+ 47 - 194
Terminal.Gui/Core/Autocomplete/Autocomplete.cs → Terminal.Gui/Core/Autocomplete/PopupAutocomplete.cs

@@ -11,12 +11,12 @@ namespace Terminal.Gui {
 	/// Renders an overlay on another view at a given point that allows selecting
 	/// from a range of 'autocomplete' options.
 	/// </summary>
-	public abstract class Autocomplete : IAutocomplete {
+	public abstract class PopupAutocomplete : AutocompleteBase {
 
 		private class Popup : View {
-			Autocomplete autocomplete;
+			PopupAutocomplete autocomplete;
 
-			public Popup (Autocomplete autocomplete)
+			public Popup (PopupAutocomplete autocomplete)
 			{
 				this.autocomplete = autocomplete;
 				CanFocus = true;
@@ -61,7 +61,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// The host control to handle.
 		/// </summary>
-		public virtual View HostControl {
+		public override View HostControl {
 			get => hostControl;
 			set {
 				hostControl = value;
@@ -74,6 +74,14 @@ namespace Terminal.Gui {
 			}
 		}
 
+		/// <summary>
+		/// Creates a new instance of the <see cref="PopupAutocomplete"/> class.
+		/// </summary>
+		public PopupAutocomplete ()
+		{
+			PopupInsideContainer = true;
+		}
+
 		private void Top_Removed (object sender, SuperViewChangedEventArgs e)
 		{
 			Visible = false;
@@ -112,55 +120,19 @@ namespace Terminal.Gui {
 			}
 		}
 
-		/// <summary>
-		/// Gets or sets If the popup is displayed inside or outside the host limits.
-		/// </summary>
-		public bool PopupInsideContainer { get; set; } = true;
-
-		/// <summary>
-		/// The maximum width of the autocomplete dropdown
-		/// </summary>
-		public virtual int MaxWidth { get; set; } = 10;
-
-		/// <summary>
-		/// The maximum number of visible rows in the autocomplete dropdown to render
-		/// </summary>
-		public virtual int MaxHeight { get; set; } = 6;
-
-		/// <summary>
-		/// True if the autocomplete should be considered open and visible
-		/// </summary>
-		public virtual bool Visible { get; set; }
-
-		/// <summary>
-		/// The strings that form the current list of suggestions to render
-		/// based on what the user has typed so far.
-		/// </summary>
-		public virtual ReadOnlyCollection<string> Suggestions { get; set; } = new ReadOnlyCollection<string> (new string [0]);
-
-		/// <summary>
-		/// The full set of all strings that can be suggested.
-		/// </summary>
-		/// <returns></returns>
-		public virtual List<string> AllSuggestions { get; set; } = new List<string> ();
-
-		/// <summary>
-		/// The currently selected index into <see cref="Suggestions"/> that the user has highlighted
-		/// </summary>
-		public virtual int SelectedIdx { get; set; }
 
 		/// <summary>
 		/// When more suggestions are available than can be rendered the user
-		/// can scroll down the dropdown list.  This indicates how far down they
+		/// can scroll down the dropdown list. This indicates how far down they
 		/// have gone
 		/// </summary>
 		public virtual int ScrollOffset { get; set; }
 
 		/// <summary>
-		/// The colors to use to render the overlay.  Accessing this property before
+		/// The colors to use to render the overlay. Accessing this property before
 		/// the Application has been initialized will cause an error
 		/// </summary>
-		public virtual ColorScheme ColorScheme {
+		public override ColorScheme ColorScheme {
 			get {
 				if (colorScheme == null) {
 					colorScheme = Colors.Menu;
@@ -172,27 +144,12 @@ namespace Terminal.Gui {
 			}
 		}
 
-		/// <summary>
-		/// The key that the user must press to accept the currently selected autocomplete suggestion
-		/// </summary>
-		public virtual Key SelectionKey { get; set; } = Key.Enter;
-
-		/// <summary>
-		/// The key that the user can press to close the currently popped autocomplete menu
-		/// </summary>
-		public virtual Key CloseKey { get; set; } = Key.Esc;
-
-		/// <summary>
-		/// The key that the user can press to reopen the currently popped autocomplete menu
-		/// </summary>
-		public virtual Key Reopen { get; set; } = Key.Space | Key.CtrlMask | Key.AltMask;
-
 		/// <summary>
 		/// Renders the autocomplete dialog inside or outside the given <see cref="HostControl"/> at the
 		/// given point.
 		/// </summary>
 		/// <param name="renderAt"></param>
-		public virtual void RenderOverlay (Point renderAt)
+		public override void RenderOverlay (Point renderAt)
 		{
 			if (!Visible || HostControl?.HasFocus == false || Suggestions.Count == 0) {
 				LastPopupPos = null;
@@ -240,7 +197,7 @@ namespace Terminal.Gui {
 				return;
 			}
 
-			width = Math.Min (MaxWidth, toRender.Max (s => s.Length));
+			width = Math.Min (MaxWidth, toRender.Max (s => s.Title.Length));
 
 			if (PopupInsideContainer) {
 				// don't overspill horizontally, let's see if can be displayed on the left
@@ -288,18 +245,17 @@ namespace Terminal.Gui {
 
 				popup.Move (0, i);
 
-				var text = TextFormatter.ClipOrPad (toRender [i], width);
+				var text = TextFormatter.ClipOrPad (toRender [i].Title, width);
 
 				Application.Driver.AddStr (text);
 			}
 		}
 
-		/// <summary>
-		/// Updates <see cref="SelectedIdx"/> to be a valid index within <see cref="Suggestions"/>
-		/// </summary>
-		public virtual void EnsureSelectedIdxIsValid ()
+		/// <inheritdoc/>
+		public override void EnsureSelectedIdxIsValid ()
 		{
-			SelectedIdx = Math.Max (0, Math.Min (Suggestions.Count - 1, SelectedIdx));
+			base.EnsureSelectedIdxIsValid ();
+
 
 			// if user moved selection up off top of current scroll window
 			if (SelectedIdx < ScrollOffset) {
@@ -311,7 +267,6 @@ namespace Terminal.Gui {
 				ScrollOffset++;
 			}
 		}
-
 		/// <summary>
 		/// Handle key events before <see cref="HostControl"/> e.g. to make key events like
 		/// up/down apply to the autocomplete control instead of changing the cursor position in
@@ -319,9 +274,9 @@ namespace Terminal.Gui {
 		/// </summary>
 		/// <param name="kb">The key event.</param>
 		/// <returns><c>true</c>if the key can be handled <c>false</c>otherwise.</returns>
-		public virtual bool ProcessKey (KeyEvent kb)
+		public override bool ProcessKey (KeyEvent kb)
 		{
-			if (IsWordChar ((char)kb.Key)) {
+			if (SuggestionGenerator.IsWordChar ((char)kb.Key)) {
 				Visible = true;
 				ManipulatePopup ();
 				closed = false;
@@ -350,7 +305,8 @@ namespace Terminal.Gui {
 				return true;
 			}
 
-			if (kb.Key == Key.CursorLeft || kb.Key == Key.CursorRight) {
+			// TODO : Revisit this
+			/*if (kb.Key == Key.CursorLeft || kb.Key == Key.CursorRight) {
 				GenerateSuggestions (kb.Key == Key.CursorLeft ? -1 : 1);
 				if (Suggestions.Count == 0) {
 					Visible = false;
@@ -359,7 +315,7 @@ namespace Terminal.Gui {
 					}
 				}
 				return false;
-			}
+			}*/
 
 			if (kb.Key == SelectionKey) {
 				return Select ();
@@ -381,13 +337,16 @@ namespace Terminal.Gui {
 		/// <param name="me">The mouse event.</param>
 		/// <param name="fromHost">If was called from the popup or from the host.</param>
 		/// <returns><c>true</c>if the mouse can be handled <c>false</c>otherwise.</returns>
-		public virtual bool MouseEvent (MouseEvent me, bool fromHost = false)
+		public override bool MouseEvent (MouseEvent me, bool fromHost = false)
 		{
 			if (fromHost) {
 				if (!Visible) {
 					return false;
 				}
-				GenerateSuggestions ();
+
+				// TODO: Revisit this
+				//GenerateSuggestions ();
+
 				if (Visible && Suggestions.Count == 0) {
 					Visible = false;
 					HostControl?.SetNeedsDisplay ();
@@ -450,56 +409,10 @@ namespace Terminal.Gui {
 			}
 		}
 
-		/// <summary>
-		/// Clears <see cref="Suggestions"/>
-		/// </summary>
-		public virtual void ClearSuggestions ()
-		{
-			Suggestions = Enumerable.Empty<string> ().ToList ().AsReadOnly ();
-		}
-
-
-		/// <summary>
-		/// Populates <see cref="Suggestions"/> with all strings in <see cref="AllSuggestions"/> that
-		/// match with the current cursor position/text in the <see cref="HostControl"/>
-		/// </summary>
-		/// <param name="columnOffset">The column offset.</param>
-		public virtual void GenerateSuggestions (int columnOffset = 0)
-		{
-			// if there is nothing to pick from
-			if (AllSuggestions.Count == 0) {
-				ClearSuggestions ();
-				return;
-			}
-
-			var currentWord = GetCurrentWord (columnOffset);
-
-			if (string.IsNullOrWhiteSpace (currentWord)) {
-				ClearSuggestions ();
-			} else {
-				Suggestions = AllSuggestions.Where (o =>
-				o.StartsWith (currentWord, StringComparison.CurrentCultureIgnoreCase) &&
-				!o.Equals (currentWord, StringComparison.CurrentCultureIgnoreCase)
-				).ToList ().AsReadOnly ();
-
-				EnsureSelectedIdxIsValid ();
-			}
-		}
-
 
-		/// <summary>
-		/// 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>
-		/// <returns></returns>
-		public virtual bool IsWordChar (Rune rune)
-		{
-			return Char.IsLetterOrDigit ((char)rune);
-		}
 
 		/// <summary>
-		/// Completes the autocomplete selection process.  Called when user hits the <see cref="SelectionKey"/>.
+		/// Completes the autocomplete selection process. Called when user hits the <see cref="IAutocomplete.SelectionKey"/>.
 		/// </summary>
 		/// <returns></returns>
 		protected bool Select ()
@@ -516,87 +429,24 @@ namespace Terminal.Gui {
 
 		/// <summary>
 		/// Called when the user confirms a selection at the current cursor location in
-		/// the <see cref="HostControl"/>.  The <paramref name="accepted"/> string
-		/// is the full autocomplete word to be inserted.  Typically a host will have to
+		/// the <see cref="HostControl"/>. The <paramref name="accepted"/> string
+		/// is the full autocomplete word to be inserted. Typically a host will have to
 		/// remove some characters such that the <paramref name="accepted"/> string 
 		/// completes the word instead of simply being appended.
 		/// </summary>
 		/// <param name="accepted"></param>
 		/// <returns>True if the insertion was possible otherwise false</returns>
-		protected virtual bool InsertSelection (string accepted)
+		protected virtual bool InsertSelection (Suggestion accepted)
 		{
-			var typedSoFar = GetCurrentWord () ?? "";
-
-			if (typedSoFar.Length < accepted.Length) {
-
-				// delete the text
-				for (int i = 0; i < typedSoFar.Length; i++) {
-					DeleteTextBackwards ();
-				}
-
-				InsertText (accepted);
-				return true;
+			// delete the text
+			for (int i = 0; i < accepted.Remove; i++) {
+				DeleteTextBackwards ();
 			}
 
-			return false;
+			InsertText (accepted.Replacement);
+			return true;
 		}
 
-		/// <summary>
-		/// Returns the currently selected word from the <see cref="HostControl"/>.
-		/// <para>
-		/// When overriding this method views can make use of <see cref="IdxToWord(List{Rune}, int, int)"/>
-		/// </para>
-		/// </summary>
-		/// <param name="columnOffset">The column offset.</param>
-		/// <returns></returns>
-		protected abstract string GetCurrentWord (int columnOffset = 0);
-
-		/// <summary>
-		/// <para>
-		/// Given a <paramref name="line"/> of characters, returns the word which ends at <paramref name="idx"/> 
-		/// or null.  Also returns null if the <paramref name="idx"/> is positioned in the middle of a word.
-		/// </para>
-		/// 
-		/// <para>
-		/// Use this method to determine whether autocomplete should be shown when the cursor is at
-		/// a given point in a line and to get the word from which suggestions should be generated.
-		/// Use the <paramref name="columnOffset"/> to indicate if search the word at left (negative),
-		/// at right (positive) or at the current column (zero) which is the default.
-		/// </para>
-		/// </summary>
-		/// <param name="line"></param>
-		/// <param name="idx"></param>
-		/// <param name="columnOffset"></param>
-		/// <returns></returns>
-		protected virtual string IdxToWord (List<Rune> line, int idx, int columnOffset = 0)
-		{
-			StringBuilder sb = new StringBuilder ();
-			var endIdx = idx;
-
-			// get the ending word index
-			while (endIdx < line.Count) {
-				if (IsWordChar (line [endIdx])) {
-					endIdx++;
-				} else {
-					break;
-				}
-			}
-
-			// It isn't a word char then there is no way to autocomplete that word
-			if (endIdx == 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]);
-				} else {
-					break;
-				}
-			}
-			return sb.ToString ();
-		}
 
 		/// <summary>
 		/// Deletes the text backwards before insert the selected text in the <see cref="HostControl"/>.
@@ -604,13 +454,13 @@ namespace Terminal.Gui {
 		protected abstract void DeleteTextBackwards ();
 
 		/// <summary>
-		/// Inser the selected text in the <see cref="HostControl"/>.
+		/// Insert the selected text in the <see cref="HostControl"/>.
 		/// </summary>
 		/// <param name="accepted"></param>
 		protected abstract void InsertText (string accepted);
 
 		/// <summary>
-		/// Closes the Autocomplete context menu if it is showing and <see cref="ClearSuggestions"/>
+		/// Closes the Autocomplete context menu if it is showing and <see cref="IAutocomplete.ClearSuggestions"/>
 		/// </summary>
 		protected void Close ()
 		{
@@ -653,7 +503,9 @@ namespace Terminal.Gui {
 		/// <returns></returns>
 		protected bool ReopenSuggestions ()
 		{
-			GenerateSuggestions ();
+			// TODO: Revisit
+			//GenerateSuggestions ();
+
 			if (Suggestions.Count > 0) {
 				Visible = true;
 				closed = false;
@@ -664,3 +516,4 @@ namespace Terminal.Gui {
 		}
 	}
 }
+

+ 105 - 0
Terminal.Gui/Core/Autocomplete/SingleWordSuggestionGenerator.cs

@@ -0,0 +1,105 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Rune = System.Rune;
+
+namespace Terminal.Gui {
+	
+	/// <summary>
+	/// <see cref="ISuggestionGenerator"/> which suggests from a collection
+	/// of words those that match the <see cref="AutocompleteContext"/>. You
+	/// can update <see cref="AllSuggestions"/> at any time to change candidates
+	/// considered for autocomplete.
+	/// </summary>
+	public class SingleWordSuggestionGenerator : ISuggestionGenerator {
+
+		/// <summary>
+		/// The full set of all strings that can be suggested.
+		/// </summary>
+		/// <returns></returns>
+		public virtual List<string> AllSuggestions { get; set; } = new List<string> ();
+
+		/// <inheritdoc/>
+		public IEnumerable<Suggestion> GenerateSuggestions (AutocompleteContext context)
+		{
+			// if there is nothing to pick from
+			if (AllSuggestions.Count == 0) {
+				return Enumerable.Empty<Suggestion> ();
+			}
+
+			var currentWord = IdxToWord (context.CurrentLine, context.CursorPosition);
+
+			if (string.IsNullOrWhiteSpace (currentWord)) {
+				return Enumerable.Empty<Suggestion> ();
+			} else {
+				return AllSuggestions.Where (o =>
+				o.StartsWith (currentWord, StringComparison.CurrentCultureIgnoreCase) &&
+				!o.Equals (currentWord, StringComparison.CurrentCultureIgnoreCase)
+				).Select (o => new Suggestion (currentWord.Length, o))
+					.ToList ().AsReadOnly ();
+
+			}
+		}
+
+		/// <summary>
+		/// 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>
+		/// <returns></returns>
+		public virtual bool IsWordChar (Rune rune)
+		{
+			return Char.IsLetterOrDigit ((char)rune);
+		}
+
+
+		/// <summary>
+		/// <para>
+		/// Given a <paramref name="line"/> of characters, returns the word which ends at <paramref name="idx"/> 
+		/// or null. Also returns null if the <paramref name="idx"/> is positioned in the middle of a word.
+		/// </para>
+		/// 
+		/// <para>
+		/// Use this method to determine whether autocomplete should be shown when the cursor is at
+		/// a given point in a line and to get the word from which suggestions should be generated.
+		/// Use the <paramref name="columnOffset"/> to indicate if search the word at left (negative),
+		/// at right (positive) or at the current column (zero) which is the default.
+		/// </para>
+		/// </summary>
+		/// <param name="line"></param>
+		/// <param name="idx"></param>
+		/// <param name="columnOffset"></param>
+		/// <returns></returns>
+		protected virtual string IdxToWord (List<Rune> line, int idx, int columnOffset = 0)
+		{
+			StringBuilder sb = new StringBuilder ();
+			var endIdx = idx;
+
+			// get the ending word index
+			while (endIdx < line.Count) {
+				if (IsWordChar (line [endIdx])) {
+					endIdx++;
+				} else {
+					break;
+				}
+			}
+
+			// It isn't a word char then there is no way to autocomplete that word
+			if (endIdx == 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]);
+				} else {
+					break;
+				}
+			}
+			return sb.ToString ();
+		}
+	}
+}
+

+ 39 - 0
Terminal.Gui/Core/Autocomplete/Suggestion.cs

@@ -0,0 +1,39 @@
+namespace Terminal.Gui {
+	/// <summary>
+	/// A replacement suggestion made by <see cref="IAutocomplete"/>
+	/// </summary>
+	public class Suggestion {
+		/// <summary>
+		/// The number of characters to remove at the current cursor position
+		/// before adding the <see cref="Replacement"/>
+		/// </summary>
+		public int Remove { get; }
+
+		/// <summary>
+		/// The user visible description for the <see cref="Replacement"/>. Typically
+		/// this would be the same as <see cref="Replacement"/> but may vary in advanced
+		/// use cases (e.g. Title= "ctor", Replacement = "MyClass()\n{\n}")
+		/// </summary>
+		public string Title { get; }
+
+		/// <summary>
+		/// The replacement text that will be added
+		/// </summary>
+		public string Replacement { get; }
+
+
+		/// <summary>
+		/// Creates a new instance of the <see cref="Suggestion"/> class.
+		/// </summary>
+		/// <param name="remove"></param>
+		/// <param name="replacement"></param>
+		/// <param name="title">User visible title for the suggestion or null if the same
+		/// as <paramref name="replacement"/>.</param>
+		public Suggestion (int remove, string replacement, string title = null)
+		{
+			Remove = remove;
+			Replacement = replacement;
+			Title = title ?? replacement;
+		}
+	}
+}

+ 15 - 13
Terminal.Gui/Views/TextField.cs

@@ -272,9 +272,9 @@ namespace Terminal.Gui {
 
 		/// <summary>
 		/// Provides autocomplete context menu based on suggestions at the current cursor
-		/// position. Populate <see cref="Autocomplete.AllSuggestions"/> to enable this feature.
+		/// position. Configure <see cref="ISuggestionGenerator"/> to enable this feature.
 		/// </summary>
-		public IAutocomplete Autocomplete { get; protected set; } = new TextFieldAutocomplete ();
+		public IAutocomplete Autocomplete { get; set; } = new TextFieldAutocomplete ();
 
 		///<inheritdoc/>
 		public override Rect Frame {
@@ -471,8 +471,9 @@ namespace Terminal.Gui {
 			if (SelectedLength > 0)
 				return;
 
+
 			// draw autocomplete
-			Autocomplete.GenerateSuggestions ();
+			GenerateSuggestions ();
 
 			var renderAt = new Point (
 				CursorPosition - ScrollOffset, 0);
@@ -480,6 +481,16 @@ namespace Terminal.Gui {
 			Autocomplete.RenderOverlay (renderAt);
 		}
 
+		private void GenerateSuggestions ()
+		{
+			var currentLine = Text.ToRuneList ();
+			var cursorPosition = Math.Min (this.CursorPosition, currentLine.Count);
+
+			Autocomplete.GenerateSuggestions(
+				new AutocompleteContext(currentLine,cursorPosition)
+				);
+		}
+
 		/// <inheritdoc/>
 		public override Attribute GetNormalColor ()
 		{
@@ -1315,7 +1326,7 @@ namespace Terminal.Gui {
 	/// from a range of 'autocomplete' options.
 	/// An implementation on a TextField.
 	/// </summary>
-	public class TextFieldAutocomplete : Autocomplete {
+	public class TextFieldAutocomplete : PopupAutocomplete {
 
 		/// <inheritdoc/>
 		protected override void DeleteTextBackwards ()
@@ -1323,15 +1334,6 @@ namespace Terminal.Gui {
 			((TextField)HostControl).DeleteCharLeft (false);
 		}
 
-		/// <inheritdoc/>
-		protected override string GetCurrentWord (int columnOffset = 0)
-		{
-			var host = (TextField)HostControl;
-			var currentLine = host.Text.ToRuneList ();
-			var cursorPosition = Math.Min (host.CursorPosition + columnOffset, currentLine.Count);
-			return IdxToWord (currentLine, cursorPosition, columnOffset);
-		}
-
 		/// <inheritdoc/>
 		protected override void InsertText (string accepted)
 		{

+ 12 - 14
Terminal.Gui/Views/TextView.cs

@@ -1146,7 +1146,7 @@ namespace Terminal.Gui {
 
 		/// <summary>
 		/// Provides autocomplete context menu based on suggestions at the current cursor
-		/// position. Populate <see cref="Autocomplete.AllSuggestions"/> to enable this feature
+		/// position. Configure <see cref="IAutocomplete.SuggestionGenerator"/> to enable this feature
 		/// </summary>
 		public IAutocomplete Autocomplete { get; protected set; } = new TextViewAutocomplete ();
 
@@ -1734,7 +1734,6 @@ namespace Terminal.Gui {
 					}
 					Height = 1;
 					LayoutStyle = lyout;
-					Autocomplete.PopupInsideContainer = false;
 					SetNeedsDisplay ();
 				} else if (multiline && savedHeight != null) {
 					var lyout = LayoutStyle;
@@ -1743,7 +1742,6 @@ namespace Terminal.Gui {
 					}
 					Height = savedHeight;
 					LayoutStyle = lyout;
-					Autocomplete.PopupInsideContainer = true;
 					SetNeedsDisplay ();
 				}
 			}
@@ -2427,7 +2425,7 @@ namespace Terminal.Gui {
 				return;
 
 			// draw autocomplete
-			Autocomplete.GenerateSuggestions ();
+			GenerateSuggestions ();
 
 			var renderAt = new Point (
 				CursorPosition.X - LeftColumn,
@@ -2438,6 +2436,15 @@ namespace Terminal.Gui {
 			Autocomplete.RenderOverlay (renderAt);
 		}
 
+		private void GenerateSuggestions ()
+		{
+			var currentLine = this.GetCurrentLine ();
+			var cursorPosition = Math.Min (this.CurrentColumn, currentLine.Count);
+			Autocomplete.GenerateSuggestions(
+				new AutocompleteContext(currentLine,cursorPosition)
+				);
+		}
+
 		/// <inheritdoc/>
 		public override Attribute GetNormalColor ()
 		{
@@ -4425,16 +4432,7 @@ namespace Terminal.Gui {
 	/// from a range of 'autocomplete' options.
 	/// An implementation on a TextView.
 	/// </summary>
-	public class TextViewAutocomplete : Autocomplete {
-
-		///<inheritdoc/>
-		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);
-		}
+	public class TextViewAutocomplete : PopupAutocomplete {
 
 		/// <inheritdoc/>
 		protected override void DeleteTextBackwards ()

+ 5 - 2
UICatalog/Scenarios/Editor.cs

@@ -544,6 +544,9 @@ namespace UICatalog.Scenarios {
 
 		private MenuItem CreateAutocomplete ()
 		{
+			var singleWordGenerator = new SingleWordSuggestionGenerator ();
+			_textView.Autocomplete.SuggestionGenerator = singleWordGenerator;
+
 			var auto = new MenuItem ();
 			auto.Title = "Autocomplete";
 			auto.CheckType |= MenuItemCheckStyle.Checked;
@@ -551,13 +554,13 @@ namespace UICatalog.Scenarios {
 			auto.Action += () => {
 				if ((bool)(auto.Checked = !auto.Checked)) {
 					// setup autocomplete with all words currently in the editor
-					_textView.Autocomplete.AllSuggestions =
+					singleWordGenerator.AllSuggestions =
 
 					Regex.Matches (_textView.Text.ToString (), "\\w+")
 					.Select (s => s.Value)
 					.Distinct ().ToList ();
 				} else {
-					_textView.Autocomplete.AllSuggestions.Clear ();
+					singleWordGenerator.AllSuggestions.Clear ();
 
 				}
 			};

+ 3 - 1
UICatalog/Scenarios/SyntaxHighlighting.cs

@@ -138,7 +138,9 @@ namespace UICatalog.Scenarios {
 				keywords.Add ("union");
 				keywords.Add ("exists");
 
-				Autocomplete.AllSuggestions = keywords.ToList ();
+				Autocomplete.SuggestionGenerator = new SingleWordSuggestionGenerator () {
+					AllSuggestions = keywords.ToList ()
+				};
 
 				magenta = Driver.MakeAttribute (Color.Magenta, Color.Black);
 				blue = Driver.MakeAttribute (Color.Cyan, Color.Black);

+ 26 - 2
UICatalog/Scenarios/Text.cs

@@ -24,11 +24,15 @@ namespace UICatalog.Scenarios {
 				Width = Dim.Percent (50) - 1,
 				Height = 2
 			};
+
+			var singleWordGenerator = new SingleWordSuggestionGenerator ();
+			textField.Autocomplete.SuggestionGenerator = singleWordGenerator;
+
 			textField.TextChanging += TextField_TextChanging;
 
 			void TextField_TextChanging (object sender, TextChangingEventArgs e)
 			{
-				textField.Autocomplete.AllSuggestions = Regex.Matches (e.NewText.ToString (), "\\w+")
+				singleWordGenerator.AllSuggestions = Regex.Matches (e.NewText.ToString (), "\\w+")
 					.Select (s => s.Value)
 					.Distinct ().ToList ();
 			}
@@ -58,7 +62,7 @@ namespace UICatalog.Scenarios {
 			// This shows how to enable autocomplete in TextView.
 			void TextView_DrawContent (object sender, DrawEventArgs e)
 			{
-				textView.Autocomplete.AllSuggestions = Regex.Matches (textView.Text.ToString (), "\\w+")
+				singleWordGenerator.AllSuggestions = Regex.Matches (textView.Text.ToString (), "\\w+")
 					.Select (s => s.Value)
 					.Distinct ().ToList ();
 			}
@@ -213,6 +217,26 @@ namespace UICatalog.Scenarios {
 			};
 
 			Win.Add (regexProviderField);
+
+			var labelAppendAutocomplete = new Label ("Append Autocomplete:") {
+				Y = Pos.Y (regexProviderField) + 2,
+				X = 1
+			};
+			var appendAutocompleteTextField = new TextField () {
+				X = Pos.Right(labelAppendAutocomplete),
+				Y = labelAppendAutocomplete.Y,
+				Width = Dim.Fill()
+			};
+			appendAutocompleteTextField.Autocomplete = new AppendAutocomplete (appendAutocompleteTextField);
+			appendAutocompleteTextField.Autocomplete.SuggestionGenerator = new SingleWordSuggestionGenerator {
+				AllSuggestions = new System.Collections.Generic.List<string>{
+					"fish", "flipper", "fin","fun","the","at","there","some","my","of","be","use","her","than","and","this","an","would","first","have","each","make","water","to","from","which","like","been","in","or","she","him","call","is","one","do","into","who","you","had","how","time","oil","that","by","their","has","its","it","word","if","look","now","he","but","will","two","find","was","not","up","more","long","for","what","other","write","down","on","all","about","go","day","are","were","out","see","did","as","we","many","number","get","with","when","then","no","come","his","your","them","way","made","they","can","these","could","may","said","so","people","part"
+				}
+			};
+
+
+			Win.Add (labelAppendAutocomplete);
+			Win.Add (appendAutocompleteTextField);
 		}
 
 		TimeField _timeField;

+ 3 - 1
UICatalog/Scenarios/TextViewAutocompletePopup.cs

@@ -26,6 +26,8 @@ namespace UICatalog.Scenarios {
 			var width = 20;
 			var text = " jamp jemp jimp jomp jump";
 
+
+
 			var menu = new MenuBar (new MenuBarItem [] {
 				new MenuBarItem ("_File", new MenuItem [] {
 					miMultiline =  new MenuItem ("_Multiline", "", () => Multiline()){CheckType = MenuItemCheckStyle.Checked},
@@ -121,7 +123,7 @@ namespace UICatalog.Scenarios {
 
 		private void SetAllSuggestions (TextView view)
 		{
-			view.Autocomplete.AllSuggestions = Regex.Matches (view.Text.ToString (), "\\w+")
+			((SingleWordSuggestionGenerator)view.Autocomplete.SuggestionGenerator).AllSuggestions = Regex.Matches (view.Text.ToString (), "\\w+")
 				.Select (s => s.Value)
 				.Distinct ().ToList ();
 		}

+ 254 - 0
UnitTests/Views/AppendAutocompleteTests.cs

@@ -0,0 +1,254 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Terminal.Gui.ViewTests {
+	public class AppendAutocompleteTests {
+		readonly ITestOutputHelper output;
+
+		public AppendAutocompleteTests (ITestOutputHelper output)
+		{
+			this.output = output;
+		}
+
+        [Fact, AutoInitShutdown]
+        public void TestAutoAppend_ShowThenAccept_MatchCase()
+        {
+			var tf = GetTextFieldsInView();
+
+			tf.Autocomplete = new AppendAutocomplete(tf);
+			var generator = (SingleWordSuggestionGenerator)tf.Autocomplete.SuggestionGenerator;
+			generator.AllSuggestions = new List<string>{"fish"};
+
+
+			tf.Redraw(tf.Bounds);
+			TestHelpers.AssertDriverContentsAre("",output);
+
+			tf.ProcessKey(new KeyEvent(Key.f,new KeyModifiers()));
+
+			tf.Redraw(tf.Bounds);
+			TestHelpers.AssertDriverContentsAre("fish",output);
+			Assert.Equal("f",tf.Text.ToString());
+
+			Application.Driver.SendKeys('\t',ConsoleKey.Tab,false,false,false);
+
+			tf.Redraw(tf.Bounds);
+			TestHelpers.AssertDriverContentsAre("fish",output);
+			Assert.Equal("fish",tf.Text.ToString());
+
+			// Tab should autcomplete but not move focus
+			Assert.Same(tf,Application.Top.Focused);
+
+			// Second tab should move focus (nothing to autocomplete)
+			Application.Driver.SendKeys('\t',ConsoleKey.Tab,false,false,false);
+			Assert.NotSame(tf,Application.Top.Focused);
+        }
+
+        [Fact, AutoInitShutdown]
+        public void TestAutoAppend_ShowThenAccept_CasesDiffer()
+        {
+			var tf = GetTextFieldsInView();
+
+			tf.Autocomplete = new AppendAutocomplete(tf);
+			var generator = (SingleWordSuggestionGenerator)tf.Autocomplete.SuggestionGenerator;
+			generator.AllSuggestions = new List<string>{"FISH"};
+
+
+			tf.Redraw(tf.Bounds);
+			TestHelpers.AssertDriverContentsAre("",output);
+			tf.ProcessKey(new KeyEvent(Key.m,new KeyModifiers()));
+			tf.ProcessKey(new KeyEvent(Key.y,new KeyModifiers()));
+			tf.ProcessKey(new KeyEvent(Key.Space,new KeyModifiers()));
+			tf.ProcessKey(new KeyEvent(Key.f,new KeyModifiers()));
+
+			// Even though there is no match on case we should still get the suggestion
+			tf.Redraw(tf.Bounds);
+			TestHelpers.AssertDriverContentsAre("my fISH",output);
+			Assert.Equal("my f",tf.Text.ToString());
+
+			// When tab completing the case of the whole suggestion should be applied
+			Application.Driver.SendKeys('\t',ConsoleKey.Tab,false,false,false);
+			tf.Redraw(tf.Bounds);
+			TestHelpers.AssertDriverContentsAre("my FISH",output);
+			Assert.Equal("my FISH",tf.Text.ToString());
+        }
+
+
+		[Fact, AutoInitShutdown]
+        public void TestAutoAppend_AfterCloseKey_NoAutocomplete()
+        {
+			var tf = GetTextFieldsInViewSuggesting("fish");
+
+			// f is typed and suggestion is "fish"
+			Application.Driver.SendKeys('f',ConsoleKey.F,false,false,false);
+			tf.Redraw(tf.Bounds);
+			TestHelpers.AssertDriverContentsAre("fish",output);
+			Assert.Equal("f",tf.Text.ToString());
+
+			// When cancelling autocomplete
+			Application.Driver.SendKeys('e',ConsoleKey.Escape,false,false,false);
+
+			// Suggestion should disapear
+			tf.Redraw(tf.Bounds);
+			TestHelpers.AssertDriverContentsAre("f",output);
+			Assert.Equal("f",tf.Text.ToString());
+
+			// Still has focus though
+			Assert.Same(tf,Application.Top.Focused);
+
+			// But can tab away
+			Application.Driver.SendKeys('\t',ConsoleKey.Tab,false,false,false);
+			Assert.NotSame(tf,Application.Top.Focused);
+        }
+
+		[Fact, AutoInitShutdown]
+        public void TestAutoAppend_AfterCloseKey_ReapearsOnLetter()
+        {
+			var tf = GetTextFieldsInViewSuggesting("fish");
+
+			// f is typed and suggestion is "fish"
+			Application.Driver.SendKeys('f',ConsoleKey.F,false,false,false);
+			tf.Redraw(tf.Bounds);
+			TestHelpers.AssertDriverContentsAre("fish",output);
+			Assert.Equal("f",tf.Text.ToString());
+
+			// When cancelling autocomplete
+			Application.Driver.SendKeys('e',ConsoleKey.Escape,false,false,false);
+
+			// Suggestion should disapear
+			tf.Redraw(tf.Bounds);
+			TestHelpers.AssertDriverContentsAre("f",output);
+			Assert.Equal("f",tf.Text.ToString());
+
+			// Should reapear when you press next letter
+			Application.Driver.SendKeys('i',ConsoleKey.I,false,false,false);
+			tf.Redraw(tf.Bounds);
+			TestHelpers.AssertDriverContentsAre("fish",output);
+			Assert.Equal("fi",tf.Text.ToString());
+        }
+
+
+		[Theory, AutoInitShutdown]
+		[InlineData("ffffffffffffffffffffffffff","ffffffffff")]
+		[InlineData("f234567890","f234567890")]
+		[InlineData("fisérables","fisérables")]
+        public void TestAutoAppendRendering_ShouldNotOverspill(string overspillUsing,string expectRender)
+        {
+			var tf = GetTextFieldsInViewSuggesting(overspillUsing);
+
+			// f is typed we should only see 'f' up to size of View (10)
+			Application.Driver.SendKeys('f',ConsoleKey.F,false,false,false);
+			tf.Redraw(tf.Bounds);
+			TestHelpers.AssertDriverContentsAre(expectRender,output);
+			Assert.Equal("f",tf.Text.ToString());
+        }
+
+
+		[Theory, AutoInitShutdown]
+		[InlineData(ConsoleKey.UpArrow)]
+		[InlineData(ConsoleKey.DownArrow)]
+        public void TestAutoAppend_CycleSelections(ConsoleKey cycleKey)
+        {
+			var tf = GetTextFieldsInViewSuggesting("fish","friend");
+
+			// f is typed and suggestion is "fish"
+			Application.Driver.SendKeys('f',ConsoleKey.F,false,false,false);
+			tf.Redraw(tf.Bounds);
+			TestHelpers.AssertDriverContentsAre("fish",output);
+			Assert.Equal("f",tf.Text.ToString());
+
+			// When cycling autocomplete
+			Application.Driver.SendKeys(' ',cycleKey,false,false,false);
+
+			tf.Redraw(tf.Bounds);
+			TestHelpers.AssertDriverContentsAre("friend",output);
+			Assert.Equal("f",tf.Text.ToString());
+
+			// Should be able to cycle in circles endlessly
+			Application.Driver.SendKeys(' ',cycleKey,false,false,false);
+			tf.Redraw(tf.Bounds);
+			TestHelpers.AssertDriverContentsAre("fish",output);
+			Assert.Equal("f",tf.Text.ToString());
+        }
+
+		[Fact, AutoInitShutdown]
+        public void TestAutoAppend_NoRender_WhenNoMatch()
+        {
+			var tf = GetTextFieldsInViewSuggesting("fish");
+
+			// f is typed and suggestion is "fish"
+			Application.Driver.SendKeys('f',ConsoleKey.F,false,false,false);
+			tf.Redraw(tf.Bounds);
+			TestHelpers.AssertDriverContentsAre("fish",output);
+			Assert.Equal("f",tf.Text.ToString());
+
+			// x is typed and suggestion should disapear
+			Application.Driver.SendKeys('x',ConsoleKey.F,false,false,false);
+			tf.Redraw(tf.Bounds);
+			TestHelpers.AssertDriverContentsAre("fx",output);
+			Assert.Equal("fx",tf.Text.ToString());
+        }
+
+
+		[Fact, AutoInitShutdown]
+        public void TestAutoAppend_NoRender_WhenCursorNotAtEnd()
+        {
+			var tf = GetTextFieldsInViewSuggesting("fish");
+
+			// f is typed and suggestion is "fish"
+			Application.Driver.SendKeys('f',ConsoleKey.F,false,false,false);
+			tf.Redraw(tf.Bounds);
+			TestHelpers.AssertDriverContentsAre("fish",output);
+			Assert.Equal("f",tf.Text.ToString());
+
+			// add a space then go back 1
+			Application.Driver.SendKeys(' ',ConsoleKey.Spacebar,false,false,false);
+			Application.Driver.SendKeys('<',ConsoleKey.LeftArrow,false,false,false);
+
+			tf.Redraw(tf.Bounds);
+			TestHelpers.AssertDriverContentsAre("f",output);
+			Assert.Equal("f ",tf.Text.ToString());
+        }
+
+
+
+		private TextField GetTextFieldsInViewSuggesting (params string[] suggestions)
+		{
+			var tf = GetTextFieldsInView();
+			
+			tf.Autocomplete = new AppendAutocomplete(tf);
+			var generator = (SingleWordSuggestionGenerator)tf.Autocomplete.SuggestionGenerator;
+			generator.AllSuggestions = suggestions.ToList();
+
+			tf.Redraw(tf.Bounds);
+			TestHelpers.AssertDriverContentsAre("",output);
+			
+			return tf;
+		}
+
+		private TextField GetTextFieldsInView ()
+		{
+            var tf = new TextField{
+				Width = 10
+			};
+            var tf2 = new TextField{
+				Y = 1,
+				Width = 10
+			};
+
+			var top = Application.Top;
+			top.Add (tf);
+			top.Add (tf2);
+
+			Application.Begin (top);
+			
+			Assert.Same(tf,top.Focused);
+
+			return tf;
+		}
+	}
+}

+ 41 - 32
UnitTests/Views/AutocompleteTests.cs

@@ -21,17 +21,22 @@ namespace Terminal.Gui.ViewTests {
 		public void Test_GenerateSuggestions_Simple ()
 		{
 			var ac = new TextViewAutocomplete ();
-			ac.AllSuggestions = new List<string> { "fish", "const", "Cobble" };
+			((SingleWordSuggestionGenerator)ac.SuggestionGenerator).AllSuggestions = new List<string> {
+				"fish",
+				"const",
+				"Cobble" };
 
 			var tv = new TextView ();
 			tv.InsertText ("co");
 
 			ac.HostControl = tv;
-			ac.GenerateSuggestions ();
+			ac.GenerateSuggestions (
+				new AutocompleteContext(
+				tv.Text.ToRuneList(),2));
 
 			Assert.Equal (2, ac.Suggestions.Count);
-			Assert.Equal ("const", ac.Suggestions [0]);
-			Assert.Equal ("Cobble", ac.Suggestions [1]);
+			Assert.Equal ("const", ac.Suggestions [0].Title);
+			Assert.Equal ("Cobble", ac.Suggestions [1].Title);
 		}
 
 		[Fact]
@@ -75,14 +80,16 @@ namespace Terminal.Gui.ViewTests {
 
 			Assert.Equal (Point.Empty, tv.CursorPosition);
 			Assert.NotNull (tv.Autocomplete);
-			Assert.Empty (tv.Autocomplete.AllSuggestions);
-			tv.Autocomplete.AllSuggestions = Regex.Matches (tv.Text.ToString (), "\\w+")
+			var g = (SingleWordSuggestionGenerator)tv.Autocomplete.SuggestionGenerator;
+
+			Assert.Empty (g.AllSuggestions);
+			g.AllSuggestions = Regex.Matches (tv.Text.ToString (), "\\w+")
 				.Select (s => s.Value)
 				.Distinct ().ToList ();
-			Assert.Equal (3, tv.Autocomplete.AllSuggestions.Count);
-			Assert.Equal ("Fortunately", tv.Autocomplete.AllSuggestions [0]);
-			Assert.Equal ("super", tv.Autocomplete.AllSuggestions [1]);
-			Assert.Equal ("feature", tv.Autocomplete.AllSuggestions [^1]);
+			Assert.Equal (3, g.AllSuggestions.Count);
+			Assert.Equal ("Fortunately", g.AllSuggestions [0]);
+			Assert.Equal ("super", g.AllSuggestions [1]);
+			Assert.Equal ("feature", g.AllSuggestions [^1]);
 			Assert.Equal (0, tv.Autocomplete.SelectedIdx);
 			Assert.Empty (tv.Autocomplete.Suggestions);
 			Assert.True (tv.ProcessKey (new KeyEvent (Key.F, new KeyModifiers ())));
@@ -90,73 +97,73 @@ namespace Terminal.Gui.ViewTests {
 			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]);
-			Assert.Equal ("feature", tv.Autocomplete.Suggestions [^1]);
+			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]);
+			Assert.Equal ("Fortunately", tv.Autocomplete.Suggestions[tv.Autocomplete.SelectedIdx].Replacement);
 			Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ())));
 			top.Redraw (tv.Bounds);
 			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]);
-			Assert.Equal ("feature", tv.Autocomplete.Suggestions [^1]);
+			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]);
+			Assert.Equal ("feature", tv.Autocomplete.Suggestions[tv.Autocomplete.SelectedIdx].Replacement);
 			Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ())));
 			top.Redraw (tv.Bounds);
 			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]);
-			Assert.Equal ("feature", tv.Autocomplete.Suggestions [^1]);
+			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]);
+			Assert.Equal ("Fortunately", tv.Autocomplete.Suggestions[tv.Autocomplete.SelectedIdx].Replacement);
 			Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ())));
 			top.Redraw (tv.Bounds);
 			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]);
-			Assert.Equal ("feature", tv.Autocomplete.Suggestions [^1]);
+			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]);
+			Assert.Equal ("feature", tv.Autocomplete.Suggestions[tv.Autocomplete.SelectedIdx].Replacement);
 			Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ())));
 			top.Redraw (tv.Bounds);
 			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]);
-			Assert.Equal ("feature", tv.Autocomplete.Suggestions [^1]);
+			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]);
+			Assert.Equal ("Fortunately", tv.Autocomplete.Suggestions[tv.Autocomplete.SelectedIdx].Replacement);
 			Assert.True (tv.Autocomplete.Visible);
 			top.Redraw (tv.Bounds);
 			Assert.True (tv.ProcessKey (new KeyEvent (tv.Autocomplete.CloseKey, new KeyModifiers ())));
 			Assert.Equal ($"F Fortunately super feature.", tv.Text);
 			Assert.Equal (new Point (1, 0), tv.CursorPosition);
 			Assert.Empty (tv.Autocomplete.Suggestions);
-			Assert.Equal (3, tv.Autocomplete.AllSuggestions.Count);
+			Assert.Equal (3, g.AllSuggestions.Count);
 			Assert.False (tv.Autocomplete.Visible);
 			top.Redraw (tv.Bounds);
 			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, tv.Autocomplete.AllSuggestions.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]);
-			Assert.Equal ("feature", tv.Autocomplete.Suggestions [^1]);
+			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]);
+			Assert.Equal ("Fortunately", tv.Autocomplete.Suggestions[tv.Autocomplete.SelectedIdx].Replacement);
 			Assert.True (tv.ProcessKey (new KeyEvent (tv.Autocomplete.CloseKey, new KeyModifiers ())));
 			Assert.Equal ($"Fortunately Fortunately super feature.", tv.Text);
 			Assert.Equal (new Point (11, 0), tv.CursorPosition);
 			Assert.Empty (tv.Autocomplete.Suggestions);
-			Assert.Equal (3, tv.Autocomplete.AllSuggestions.Count);
+			Assert.Equal (3, g.AllSuggestions.Count);
 		}
 
 		[Fact, AutoInitShutdown]
@@ -167,7 +174,9 @@ namespace Terminal.Gui.ViewTests {
 				Height = 5,
 				Text = "This a long line and against TextView."
 			};
-			tv.Autocomplete.AllSuggestions = Regex.Matches (tv.Text.ToString (), "\\w+")
+
+			var g = (SingleWordSuggestionGenerator)tv.Autocomplete.SuggestionGenerator;
+			g.AllSuggestions = Regex.Matches (tv.Text.ToString (), "\\w+")
 					.Select (s => s.Value)
 					.Distinct ().ToList ();
 			var top = Application.Top;

+ 16 - 14
UnitTests/Views/TextViewTests.cs

@@ -2497,6 +2497,8 @@ line.
 			Assert.False (tv.ReadOnly);
 			Assert.True (tv.CanFocus);
 
+			var g = (SingleWordSuggestionGenerator)tv.Autocomplete.SuggestionGenerator;
+
 			tv.CanFocus = false;
 			Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ())));
 			tv.CanFocus = true;
@@ -2510,7 +2512,7 @@ line.
 			Assert.Equal (new Point (23, 2), tv.CursorPosition);
 			Assert.False (tv.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
 			Assert.NotNull (tv.Autocomplete);
-			Assert.Empty (tv.Autocomplete.AllSuggestions);
+			Assert.Empty (g.AllSuggestions);
 			Assert.True (tv.ProcessKey (new KeyEvent (Key.F, new KeyModifiers ())));
 			tv.Redraw (tv.Bounds);
 			Assert.Equal ($"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.F", tv.Text);
@@ -2529,31 +2531,31 @@ line.
 			Assert.True (tv.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers ())));
 			Assert.Equal ($"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.", tv.Text);
 			Assert.Equal (new Point (23, 2), tv.CursorPosition);
-			tv.Autocomplete.AllSuggestions = Regex.Matches (tv.Text.ToString (), "\\w+")
+			g.AllSuggestions = Regex.Matches (tv.Text.ToString (), "\\w+")
 				.Select (s => s.Value)
 				.Distinct ().ToList ();
-			Assert.Equal (7, tv.Autocomplete.AllSuggestions.Count);
-			Assert.Equal ("This", tv.Autocomplete.AllSuggestions [0]);
-			Assert.Equal ("is", tv.Autocomplete.AllSuggestions [1]);
-			Assert.Equal ("the", tv.Autocomplete.AllSuggestions [2]);
-			Assert.Equal ("first", tv.Autocomplete.AllSuggestions [3]);
-			Assert.Equal ("line", tv.Autocomplete.AllSuggestions [4]);
-			Assert.Equal ("second", tv.Autocomplete.AllSuggestions [5]);
-			Assert.Equal ("third", tv.Autocomplete.AllSuggestions [^1]);
+			Assert.Equal (7, g.AllSuggestions.Count);
+			Assert.Equal ("This", g.AllSuggestions [0]);
+			Assert.Equal ("is", g.AllSuggestions [1]);
+			Assert.Equal ("the", g.AllSuggestions [2]);
+			Assert.Equal ("first", g.AllSuggestions [3]);
+			Assert.Equal ("line", g.AllSuggestions [4]);
+			Assert.Equal ("second", g.AllSuggestions [5]);
+			Assert.Equal ("third", g.AllSuggestions [^1]);
 			Assert.True (tv.ProcessKey (new KeyEvent (Key.F, new KeyModifiers ())));
 			tv.Redraw (tv.Bounds);
 			Assert.Equal ($"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.F", tv.Text);
 			Assert.Equal (new Point (24, 2), tv.CursorPosition);
 			Assert.Single (tv.Autocomplete.Suggestions);
-			Assert.Equal ("first", tv.Autocomplete.Suggestions [0]);
+			Assert.Equal ("first", tv.Autocomplete.Suggestions[0].Replacement);
 			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]);
-			tv.Autocomplete.AllSuggestions = new List<string> ();
+			Assert.Equal ("first", tv.Autocomplete.Suggestions[0].Replacement);
+			g.AllSuggestions = new List<string> ();
 			tv.Autocomplete.ClearSuggestions ();
-			Assert.Empty (tv.Autocomplete.AllSuggestions);
+			Assert.Empty (g.AllSuggestions);
 			Assert.Empty (tv.Autocomplete.Suggestions);
 			Assert.True (tv.ProcessKey (new KeyEvent (Key.PageUp, new KeyModifiers ())));
 			Assert.Equal (24, tv.GetCurrentLine ().Count);