Ver código fonte

Merge branch 'v2_develop' into caption

Thomas Nind 2 anos atrás
pai
commit
3a64cfbdb8

+ 0 - 18
Terminal.Gui/Core/Application.cs

@@ -1547,7 +1547,6 @@ namespace Terminal.Gui {
 		static void TerminalResized ()
 		{
 			var full = new Rect (0, 0, Driver.Cols, Driver.Rows);
-			SetToplevelsSize (full);
 			Resized?.Invoke (new ResizedEventArgs () { Cols = full.Width, Rows = full.Height });
 			Driver.Clip = full;
 			foreach (var t in toplevels) {
@@ -1559,23 +1558,6 @@ namespace Terminal.Gui {
 			Refresh ();
 		}
 
-		static void SetToplevelsSize (Rect full)
-		{
-			if (MdiTop == null) {
-				foreach (var t in toplevels) {
-					if (t?.SuperView == null && !t.Modal) {
-						t.Frame = full;
-						t.Width = full.Width;
-						t.Height = full.Height;
-					}
-				}
-			} else {
-				Top.Frame = full;
-				Top.Width = full.Width;
-				Top.Height = full.Height;
-			}
-		}
-
 		static bool SetCurrentAsTop ()
 		{
 			if (MdiTop == null && Current != Top && Current?.SuperView == null && Current?.Modal == false) {

+ 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;
+		}
+	}
+}

+ 19 - 8
Terminal.Gui/Core/View.cs

@@ -983,7 +983,7 @@ namespace Terminal.Gui {
 			}
 			SetNeedsLayout ();
 			SetNeedsDisplay ();
-      
+
 			OnAdded (new SuperViewChangedEventArgs (this, view));
 			if (IsInitialized && !view.IsInitialized) {
 				view.BeginInit ();
@@ -1037,9 +1037,6 @@ namespace Terminal.Gui {
 			view.tabIndex = -1;
 			SetNeedsLayout ();
 			SetNeedsDisplay ();
-			if (subviews.Count < 1) {
-				CanFocus = false;
-			}
 			foreach (var v in subviews) {
 				if (v.Frame.IntersectsWith (touched))
 					view.SetNeedsDisplay ();
@@ -1676,7 +1673,14 @@ namespace Terminal.Gui {
 				return;
 			if (focused?.hasFocus == true && focused == view)
 				return;
+			if ((focused?.hasFocus == true && focused?.SuperView == view)
+				|| view == this) {
 
+				if (!view.hasFocus) {
+					view.hasFocus = true;
+				}
+				return;
+			}
 			// Make sure that this view is a subview
 			View c;
 			for (c = view.container; c != null; c = c.container)
@@ -1694,7 +1698,11 @@ namespace Terminal.Gui {
 			focused.EnsureFocus ();
 
 			// Send focus upwards
-			SuperView?.SetFocus (this);
+			if (SuperView != null) {
+				SuperView.SetFocus (this);
+			} else {
+				SetFocus (this);
+			}
 		}
 
 		/// <summary>
@@ -1709,7 +1717,11 @@ namespace Terminal.Gui {
 				return;
 			}
 
-			SuperView?.SetFocus (this);
+			if (SuperView != null) {
+				SuperView.SetFocus (this);
+			} else {
+				SetFocus (this);
+			}
 		}
 
 		/// <summary>
@@ -2129,9 +2141,8 @@ namespace Terminal.Gui {
 				FocusFirst ();
 				return focused != null;
 			}
-			var n = tabIndexes.Count;
 			var focusedIdx = -1;
-			for (var i = 0; i < n; i++) {
+			for (var i = 0; i < tabIndexes.Count; i++) {
 				var w = tabIndexes [i];
 
 				if (w.HasFocus) {

+ 1 - 1
Terminal.Gui/Core/Window.cs

@@ -100,7 +100,7 @@ namespace Terminal.Gui {
 
 			public override void OnCanFocusChanged ()
 			{
-				if (MostFocused == null && CanFocus && Visible) {
+				if (IsInitialized && MostFocused == null && CanFocus && Visible) {
 					EnsureFocus ();
 				}
 

+ 75 - 0
Terminal.Gui/Views/SpinnerView.cs

@@ -0,0 +1,75 @@
+using System;
+
+namespace Terminal.Gui {
+
+	/// <summary>
+	/// A 1x1 <see cref="View"/> based on <see cref="Label"/> which displays a spinning
+	/// line character.
+	/// </summary>
+	/// <remarks>
+	/// By default animation only occurs when you call <see cref="View.SetNeedsDisplay()"/>.
+	/// Use <see cref="AutoSpin"/> to make the automate calls to <see cref="View.SetNeedsDisplay()"/>.
+	/// </remarks>
+	public class SpinnerView : Label {
+		private Rune [] _runes = new Rune [] { '|', '/', '\u2500', '\\' };
+		private int _currentIdx = 0;
+		private DateTime _lastRender = DateTime.MinValue;
+		private object _timeout;
+
+		/// <summary>
+		/// Gets or sets the number of milliseconds to wait between characters
+		/// in the spin.  Defaults to 250.
+		/// </summary>
+		/// <remarks>This is the maximum speed the spinner will rotate at.  You still need to
+		/// call <see cref="View.SetNeedsDisplay()"/> or <see cref="SpinnerView.AutoSpin"/> to
+		/// advance/start animation.</remarks>
+		public int SpinDelayInMilliseconds { get; set; } = 250;
+
+		/// <summary>
+		/// Creates a new instance of the <see cref="SpinnerView"/> class.
+		/// </summary>
+		public SpinnerView ()
+		{
+			Width = 1; Height = 1;
+		}
+
+		/// <inheritdoc/>
+		public override void Redraw (Rect bounds)
+		{
+			if (DateTime.Now - _lastRender > TimeSpan.FromMilliseconds (SpinDelayInMilliseconds)) {
+				_currentIdx = (_currentIdx + 1) % _runes.Length;
+				Text = "" + _runes [_currentIdx];
+				_lastRender = DateTime.Now;
+			}
+
+			base.Redraw (bounds);
+		}
+
+		/// <summary>
+		/// Automates spinning
+		/// </summary>
+		public void AutoSpin ()
+		{
+			if (_timeout != null) {
+				return;
+			}
+
+			_timeout = Application.MainLoop.AddTimeout (
+				TimeSpan.FromMilliseconds (SpinDelayInMilliseconds), (m) => {
+					Application.MainLoop.Invoke (this.SetNeedsDisplay);
+					return true;
+				});
+		}
+
+		/// <inheritdoc/>
+		protected override void Dispose (bool disposing)
+		{
+			if (_timeout != null) {
+				Application.MainLoop.RemoveTimeout (_timeout);
+				_timeout = null;
+			}
+
+			base.Dispose (disposing);
+		}
+	}
+}

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

@@ -1,4 +1,4 @@
-//
+//
 // TextField.cs: single-line text editor with Emacs keybindings
 //
 // Authors:
@@ -285,9 +285,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 {
@@ -486,8 +486,9 @@ namespace Terminal.Gui {
 			if (SelectedLength > 0)
 				return;
 
+
 			// draw autocomplete
-			Autocomplete.GenerateSuggestions ();
+			GenerateSuggestions ();
 
 			var renderAt = new Point (
 				CursorPosition - ScrollOffset, 0);
@@ -515,6 +516,15 @@ namespace Terminal.Gui {
 
 			Driver.AddStr (render);
 		}
+		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 ()
@@ -1351,7 +1361,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 ()
@@ -1359,15 +1369,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 ();
 
 				}
 			};

+ 23 - 0
UICatalog/Scenarios/Progress.cs

@@ -3,6 +3,7 @@ using System;
 using System.Threading;
 using Terminal.Gui;
 using System.Linq;
+using System.Runtime.CompilerServices;
 
 namespace UICatalog.Scenarios {
 	// 
@@ -20,6 +21,7 @@ namespace UICatalog.Scenarios {
 			internal TextField Speed { get; private set; }
 			internal ProgressBar ActivityProgressBar { get; private set; }
 			internal ProgressBar PulseProgressBar { get; private set; }
+			internal SpinnerView Spinner { get; private set; }
 			internal Action StartBtnClick;
 			internal Action StopBtnClick;
 			internal Action PulseBtnClick = null;
@@ -84,6 +86,14 @@ namespace UICatalog.Scenarios {
 				};
 				Add (ActivityProgressBar);
 
+				Spinner = new SpinnerView {
+					X = Pos.Right (ActivityProgressBar),
+					Y = ActivityProgressBar.Y,
+					Visible = false,
+
+				};
+				Add (Spinner);
+
 				PulseProgressBar = new ProgressBar () {
 					X = Pos.Right (LeftFrame) + 1,
 					Y = Pos.Bottom (ActivityProgressBar) + 1,
@@ -109,12 +119,23 @@ namespace UICatalog.Scenarios {
 			{
 				Started = true;
 				StartBtnClick?.Invoke ();
+				Application.MainLoop.Invoke(()=>{
+					Spinner.Visible = true;
+					ActivityProgressBar.Width = Dim.Fill(1);
+					this.LayoutSubviews();
+				});
 			}
 
 			internal void Stop ()
 			{
 				Started = false;
 				StopBtnClick?.Invoke ();
+
+				Application.MainLoop.Invoke(()=>{
+					Spinner.Visible = false;
+					ActivityProgressBar.Width = Dim.Fill();
+					this.LayoutSubviews();
+				});
 			}
 
 			internal void Pulse ()
@@ -129,6 +150,7 @@ namespace UICatalog.Scenarios {
 						ActivityProgressBar.Fraction += 0.01F;
 					}
 					PulseProgressBar.Pulse ();
+					Spinner.SetNeedsDisplay ();
 				}
 			}
 		}
@@ -196,6 +218,7 @@ namespace UICatalog.Scenarios {
 
 				_mainLoopTimeout = Application.MainLoop.AddTimeout (TimeSpan.FromMilliseconds (_mainLooopTimeoutTick), (loop) => {
 					mainLoopTimeoutDemo.Pulse ();
+					
 					return true;
 				});
 			};

+ 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 ();
 		}

+ 127 - 31
UnitTests/Core/ViewTests.cs

@@ -238,7 +238,7 @@ namespace Terminal.Gui.CoreTests {
 			var v = new View (new Rect (0, 0, 10, 24));
 			var t = new View ();
 
-			v.Added += (s,e) => {
+			v.Added += (s, e) => {
 				Assert.Same (v.SuperView, e.Parent);
 				Assert.Same (t, e.Parent);
 				Assert.Same (v, e.Child);
@@ -658,7 +658,7 @@ namespace Terminal.Gui.CoreTests {
 
 			int tc = 0, wc = 0, v1c = 0, v2c = 0, sv1c = 0;
 
-			w.Added += (s,e) => {
+			w.Added += (s, e) => {
 				Assert.Equal (e.Parent.Frame.Width, w.Frame.Width);
 				Assert.Equal (e.Parent.Frame.Height, w.Frame.Height);
 			};
@@ -1232,10 +1232,10 @@ namespace Terminal.Gui.CoreTests {
 			Assert.Equal (80, view.Bounds.Width);
 			Assert.Equal (25, view.Bounds.Height);
 			bool layoutStarted = false;
-			view.LayoutStarted += (s,e) => layoutStarted = true;
+			view.LayoutStarted += (s, e) => layoutStarted = true;
 			view.OnLayoutStarted (null);
 			Assert.True (layoutStarted);
-			view.LayoutComplete += (s,e) => layoutStarted = false;
+			view.LayoutComplete += (s, e) => layoutStarted = false;
 			view.OnLayoutComplete (null);
 			Assert.False (layoutStarted);
 			view.X = Pos.Center () - 41;
@@ -1252,7 +1252,7 @@ namespace Terminal.Gui.CoreTests {
 		{
 			var wasClicked = false;
 			var view = new Button ("Click Me");
-			view.Clicked += (s,e) => wasClicked = !wasClicked;
+			view.Clicked += (s, e) => wasClicked = !wasClicked;
 			Application.Top.Add (view);
 
 			view.ProcessKey (new KeyEvent (Key.Enter, null));
@@ -1281,7 +1281,7 @@ namespace Terminal.Gui.CoreTests {
 		{
 			var wasClicked = false;
 			var button = new Button ("Click Me");
-			button.Clicked += (s,e) => wasClicked = !wasClicked;
+			button.Clicked += (s, e) => wasClicked = !wasClicked;
 			var win = new Window () { Width = Dim.Fill (), Height = Dim.Fill () };
 			win.Add (button);
 			Application.Top.Add (win);
@@ -1452,11 +1452,11 @@ namespace Terminal.Gui.CoreTests {
 		[AutoInitShutdown]
 		public void CanFocus_Sets_To_False_On_Single_View_Focus_View_On_Another_Toplevel ()
 		{
-			var view1 = new View () { Width = 10, Height = 1, CanFocus = true };
-			var win1 = new Window () { Width = Dim.Percent (50), Height = Dim.Fill () };
+			var view1 = new View () { Id = "view1", Width = 10, Height = 1, CanFocus = true };
+			var win1 = new Window () { Id = "win1", Width = Dim.Percent (50), Height = Dim.Fill () };
 			win1.Add (view1);
-			var view2 = new View () { Width = 20, Height = 2, CanFocus = true };
-			var win2 = new Window () { X = Pos.Right (win1), Width = Dim.Fill (), Height = Dim.Fill () };
+			var view2 = new View () { Id = "view2", Width = 20, Height = 2, CanFocus = true };
+			var win2 = new Window () { Id = "win2", X = Pos.Right (win1), Width = Dim.Fill (), Height = Dim.Fill () };
 			win2.Add (view2);
 			Application.Top.Add (win1, win2);
 			Application.Begin (Application.Top);
@@ -1464,7 +1464,7 @@ namespace Terminal.Gui.CoreTests {
 			Assert.True (view1.CanFocus);
 			Assert.True (view1.HasFocus);
 			Assert.True (view2.CanFocus);
-			Assert.True (view2.HasFocus);
+			Assert.False (view2.HasFocus);
 
 			view1.CanFocus = false;
 			Assert.False (view1.CanFocus);
@@ -1477,12 +1477,12 @@ namespace Terminal.Gui.CoreTests {
 		[AutoInitShutdown]
 		public void CanFocus_Sets_To_False_With_Two_Views_Focus_Another_View_On_The_Same_Toplevel ()
 		{
-			var view1 = new View () { Width = 10, Height = 1, CanFocus = true };
-			var view12 = new View () { Y = 5, Width = 10, Height = 1, CanFocus = true };
-			var win1 = new Window () { Width = Dim.Percent (50), Height = Dim.Fill () };
+			var view1 = new View () { Id = "view1", Width = 10, Height = 1, CanFocus = true };
+			var view12 = new View () { Id = "view12", Y = 5, Width = 10, Height = 1, CanFocus = true };
+			var win1 = new Window () { Id = "win1", Width = Dim.Percent (50), Height = Dim.Fill () };
 			win1.Add (view1, view12);
-			var view2 = new View () { Width = 20, Height = 2, CanFocus = true };
-			var win2 = new Window () { X = Pos.Right (win1), Width = Dim.Fill (), Height = Dim.Fill () };
+			var view2 = new View () { Id = "view2", Width = 20, Height = 2, CanFocus = true };
+			var win2 = new Window () { Id = "win2", X = Pos.Right (win1), Width = Dim.Fill (), Height = Dim.Fill () };
 			win2.Add (view2);
 			Application.Top.Add (win1, win2);
 			Application.Begin (Application.Top);
@@ -1490,7 +1490,7 @@ namespace Terminal.Gui.CoreTests {
 			Assert.True (view1.CanFocus);
 			Assert.True (view1.HasFocus);
 			Assert.True (view2.CanFocus);
-			Assert.True (view2.HasFocus);
+			Assert.False (view2.HasFocus);
 
 			view1.CanFocus = false;
 			Assert.False (view1.CanFocus);
@@ -1503,11 +1503,11 @@ namespace Terminal.Gui.CoreTests {
 		[AutoInitShutdown]
 		public void CanFocus_Sets_To_False_On_Toplevel_Focus_View_On_Another_Toplevel ()
 		{
-			var view1 = new View () { Width = 10, Height = 1, CanFocus = true };
-			var win1 = new Window () { Width = Dim.Percent (50), Height = Dim.Fill () };
+			var view1 = new View () { Id = "view1", Width = 10, Height = 1, CanFocus = true };
+			var win1 = new Window () { Id = "win1", Width = Dim.Percent (50), Height = Dim.Fill () };
 			win1.Add (view1);
-			var view2 = new View () { Width = 20, Height = 2, CanFocus = true };
-			var win2 = new Window () { X = Pos.Right (win1), Width = Dim.Fill (), Height = Dim.Fill () };
+			var view2 = new View () { Id = "view2", Width = 20, Height = 2, CanFocus = true };
+			var win2 = new Window () { Id = "win2", X = Pos.Right (win1), Width = Dim.Fill (), Height = Dim.Fill () };
 			win2.Add (view2);
 			Application.Top.Add (win1, win2);
 			Application.Begin (Application.Top);
@@ -1515,7 +1515,7 @@ namespace Terminal.Gui.CoreTests {
 			Assert.True (view1.CanFocus);
 			Assert.True (view1.HasFocus);
 			Assert.True (view2.CanFocus);
-			Assert.True (view2.HasFocus);
+			Assert.False (view2.HasFocus);
 
 			win1.CanFocus = false;
 			Assert.False (view1.CanFocus);
@@ -2025,7 +2025,7 @@ namespace Terminal.Gui.CoreTests {
 				Width = Dim.Fill (),
 				Height = Dim.Fill ()
 			};
-			view.DrawContent += (s,e) => {
+			view.DrawContent += (s, e) => {
 				view.DrawFrame (view.Bounds);
 				var savedClip = Application.Driver.Clip;
 				Application.Driver.Clip = new Rect (1, 1, view.Bounds.Width - 2, view.Bounds.Height - 2);
@@ -2073,7 +2073,7 @@ namespace Terminal.Gui.CoreTests {
 				Width = Dim.Fill (),
 				Height = Dim.Fill ()
 			};
-			view.DrawContent += (s,e) => {
+			view.DrawContent += (s, e) => {
 				view.DrawFrame (view.Bounds);
 				var savedClip = Application.Driver.Clip;
 				Application.Driver.Clip = new Rect (1, 1, view.Bounds.Width - 2, view.Bounds.Height - 2);
@@ -2268,7 +2268,7 @@ This is a tes
 			var tvCalled = false;
 
 			var view = new View ("View") { Width = 10, Height = 10 };
-			view.DrawContentComplete += (s,e) => viewCalled = true;
+			view.DrawContentComplete += (s, e) => viewCalled = true;
 			var tv = new TextView () { Y = 11, Width = 10, Height = 10 };
 			tv.DrawContentComplete += (s, e) => tvCalled = true;
 
@@ -2294,7 +2294,7 @@ This is a tes
 				e.Handled = true;
 				keyDown = true;
 			};
-			view.KeyPress += (s,e) => {
+			view.KeyPress += (s, e) => {
 				Assert.Equal (Key.a, e.KeyEvent.Key);
 				Assert.False (keyPress);
 				Assert.False (view.IsKeyPress);
@@ -2390,7 +2390,7 @@ This is a tes
 			var keyUp = false;
 
 			var view = new DerivedView ();
-			view.KeyDown += (s,e) => {
+			view.KeyDown += (s, e) => {
 				Assert.Equal (-1, e.KeyEvent.KeyValue);
 				Assert.Equal (shift, e.KeyEvent.IsShift);
 				Assert.Equal (alt, e.KeyEvent.IsAlt);
@@ -2441,15 +2441,15 @@ This is a tes
 			var view1 = new View { CanFocus = true };
 			var subView1 = new View { CanFocus = true };
 			var subView1subView1 = new View { CanFocus = true };
-			view1.Leave += (s,e) => {
+			view1.Leave += (s, e) => {
 				view1Leave = true;
 			};
-			subView1.Leave += (s,e) => {
+			subView1.Leave += (s, e) => {
 				subView1.Remove (subView1subView1);
 				subView1Leave = true;
 			};
 			view1.Add (subView1);
-			subView1subView1.Leave += (s,e) => {
+			subView1subView1.Leave += (s, e) => {
 				// This is never invoked
 				subView1subView1Leave = true;
 			};
@@ -2908,7 +2908,7 @@ At 0,0
 			Assert.Equal (new Rect (0, 0, 30, 1), label.NeedDisplay);
 			Assert.Equal (new Rect (0, 0, 13, 1), button.NeedDisplay);
 
-			top.LayoutComplete += (s,e) => {
+			top.LayoutComplete += (s, e) => {
 				Assert.Equal (new Rect (0, 0, 80, 25), top.NeedDisplay);
 			};
 
@@ -2957,5 +2957,101 @@ At 0,0
 
 			TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
 		}
+
+		[Fact, AutoInitShutdown]
+		public void Remove_Does_Not_Change_Focus ()
+		{
+			Assert.True (Application.Top.CanFocus);
+			Assert.False (Application.Top.HasFocus);
+
+			var container = new View () { Width = 10, Height = 10 };
+			var leave = false;
+			container.Leave += (s, e) => leave = true;
+			Assert.False (container.CanFocus);
+			var child = new View () { Width = Dim.Fill (), Height = Dim.Fill (), CanFocus = true };
+			container.Add (child);
+
+			Assert.True (container.CanFocus);
+			Assert.False (container.HasFocus);
+			Assert.True (child.CanFocus);
+			Assert.False (child.HasFocus);
+
+			Application.Top.Add (container);
+			Application.Begin (Application.Top);
+
+			Assert.True (Application.Top.CanFocus);
+			Assert.True (Application.Top.HasFocus);
+			Assert.True (container.CanFocus);
+			Assert.True (container.HasFocus);
+			Assert.True (child.CanFocus);
+			Assert.True (child.HasFocus);
+
+			container.Remove (child);
+			child.Dispose ();
+			child = null;
+			Assert.True (Application.Top.HasFocus);
+			Assert.True (container.CanFocus);
+			Assert.True (container.HasFocus);
+			Assert.Null (child);
+			Assert.False (leave);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void SetFocus_View_With_Null_Superview_Does_Not_Throw_Exception ()
+		{
+			Assert.True (Application.Top.CanFocus);
+			Assert.False (Application.Top.HasFocus);
+
+			var exception = Record.Exception (Application.Top.SetFocus);
+			Assert.Null (exception);
+			Assert.True (Application.Top.CanFocus);
+			Assert.True (Application.Top.HasFocus);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void FocusNext_Does_Not_Throws_If_A_View_Was_Removed_From_The_Collection ()
+		{
+			var top1 = Application.Top;
+			var view1 = new View () { Id = "view1", Width = 10, Height = 5, CanFocus = true };
+			var top2 = new Toplevel () { Id = "top2", Y = 1, Width = 10, Height = 5 };
+			var view2 = new View () { Id = "view2", Y = 1, Width = 10, Height = 5, CanFocus = true };
+			View view3 = null;
+			var removed = false;
+			view2.Enter += (s, e) => {
+				if (!removed) {
+					removed = true;
+					view3 = new View () { Id = "view3", Y = 1, Width = 10, Height = 5 };
+					Application.Current.Add (view3);
+					Application.Current.BringSubviewToFront (view3);
+					Assert.False (view3.HasFocus);
+				}
+			};
+			view2.Leave += (s, e) => {
+				Application.Current.Remove (view3);
+				view3.Dispose ();
+				view3 = null;
+			};
+			top2.Add (view2);
+			top1.Add (view1, top2);
+			Application.Begin (top1);
+
+			Assert.True (top1.HasFocus);
+			Assert.True (view1.HasFocus);
+			Assert.False (view2.HasFocus);
+			Assert.False (removed);
+			Assert.Null (view3);
+
+			Assert.True (top1.ProcessKey (new KeyEvent (Key.Tab | Key.CtrlMask, new KeyModifiers { Ctrl = true })));
+			Assert.True (top1.HasFocus);
+			Assert.False (view1.HasFocus);
+			Assert.True (view2.HasFocus);
+			Assert.True (removed);
+			Assert.NotNull (view3);
+
+			var exception = Record.Exception (() => top1.ProcessKey (new KeyEvent (Key.Tab | Key.CtrlMask, new KeyModifiers { Ctrl = true })));
+			Assert.Null (exception);
+			Assert.True (removed);
+			Assert.Null (view3);
+		}
 	}
 }

+ 14 - 14
UnitTests/TopLevels/ToplevelTests.cs

@@ -340,22 +340,22 @@ namespace Terminal.Gui.TopLevelTests {
 		{
 			var isRunning = false;
 
-			var win1 = new Window ("Win1") { Width = Dim.Percent (50f), Height = Dim.Fill () };
-			var lblTf1W1 = new Label ("Enter text in TextField on Win1:");
-			var tf1W1 = new TextField ("Text1 on Win1") { X = Pos.Right (lblTf1W1) + 1, Width = Dim.Fill () };
-			var lblTvW1 = new Label ("Enter text in TextView on Win1:") { Y = Pos.Bottom (lblTf1W1) + 1 };
-			var tvW1 = new TextView () { X = Pos.Left (tf1W1), Width = Dim.Fill (), Height = 2, Text = "First line Win1\nSecond line Win1" };
-			var lblTf2W1 = new Label ("Enter text in TextField on Win1:") { Y = Pos.Bottom (lblTvW1) + 1 };
-			var tf2W1 = new TextField ("Text2 on Win1") { X = Pos.Left (tf1W1), Width = Dim.Fill () };
+			var win1 = new Window ("Win1") { Id = "win1", Width = Dim.Percent (50f), Height = Dim.Fill () };
+			var lblTf1W1 = new Label ("Enter text in TextField on Win1:") { Id = "lblTf1W1" };
+			var tf1W1 = new TextField ("Text1 on Win1") { Id = "tf1W1", X = Pos.Right (lblTf1W1) + 1, Width = Dim.Fill () };
+			var lblTvW1 = new Label ("Enter text in TextView on Win1:") { Id = "lblTvW1", Y = Pos.Bottom (lblTf1W1) + 1 };
+			var tvW1 = new TextView () { Id = "tvW1", X = Pos.Left (tf1W1), Width = Dim.Fill (), Height = 2, Text = "First line Win1\nSecond line Win1" };
+			var lblTf2W1 = new Label ("Enter text in TextField on Win1:") { Id = "lblTf2W1", Y = Pos.Bottom (lblTvW1) + 1 };
+			var tf2W1 = new TextField ("Text2 on Win1") { Id = "tf2W1", X = Pos.Left (tf1W1), Width = Dim.Fill () };
 			win1.Add (lblTf1W1, tf1W1, lblTvW1, tvW1, lblTf2W1, tf2W1);
 
-			var win2 = new Window ("Win2") { X = Pos.Right (win1) + 1, Width = Dim.Percent (50f), Height = Dim.Fill () };
-			var lblTf1W2 = new Label ("Enter text in TextField on Win2:");
-			var tf1W2 = new TextField ("Text1 on Win2") { X = Pos.Right (lblTf1W2) + 1, Width = Dim.Fill () };
-			var lblTvW2 = new Label ("Enter text in TextView on Win2:") { Y = Pos.Bottom (lblTf1W2) + 1 };
-			var tvW2 = new TextView () { X = Pos.Left (tf1W2), Width = Dim.Fill (), Height = 2, Text = "First line Win1\nSecond line Win2" };
-			var lblTf2W2 = new Label ("Enter text in TextField on Win2:") { Y = Pos.Bottom (lblTvW2) + 1 };
-			var tf2W2 = new TextField ("Text2 on Win2") { X = Pos.Left (tf1W2), Width = Dim.Fill () };
+			var win2 = new Window ("Win2") { Id = "win2", X = Pos.Right (win1) + 1, Width = Dim.Percent (50f), Height = Dim.Fill () };
+			var lblTf1W2 = new Label ("Enter text in TextField on Win2:") { Id = "lblTf1W2" };
+			var tf1W2 = new TextField ("Text1 on Win2") { Id = "tf1W2", X = Pos.Right (lblTf1W2) + 1, Width = Dim.Fill () };
+			var lblTvW2 = new Label ("Enter text in TextView on Win2:") { Id = "lblTvW2", Y = Pos.Bottom (lblTf1W2) + 1 };
+			var tvW2 = new TextView () { Id = "tvW2", X = Pos.Left (tf1W2), Width = Dim.Fill (), Height = 2, Text = "First line Win1\nSecond line Win2" };
+			var lblTf2W2 = new Label ("Enter text in TextField on Win2:") { Id = "lblTf2W2", Y = Pos.Bottom (lblTvW2) + 1 };
+			var tf2W2 = new TextField ("Text2 on Win2") { Id = "tf2W2", X = Pos.Left (tf1W2), Width = Dim.Fill () };
 			win2.Add (lblTf1W2, tf1W2, lblTvW2, tvW2, lblTf2W2, tf2W2);
 
 			var top = Application.Top;

+ 18 - 1
UnitTests/TopLevels/WindowTests.cs

@@ -138,7 +138,7 @@ namespace Terminal.Gui.TopLevelTests {
 
 			string expectedOld = null;
 			string expected = null;
-			r.TitleChanged += (s,args) => {
+			r.TitleChanged += (s, args) => {
 				Assert.Equal (expectedOld, args.OldTitle);
 				Assert.Equal (r.Title, args.NewTitle);
 			};
@@ -235,7 +235,24 @@ namespace Terminal.Gui.TopLevelTests {
 │└────────────────┘│
 │ ^Q Quit │ ^O Open│
 └──────────────────┘", output);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void OnCanFocusChanged_Only_Must_ContentView_Forces_SetFocus_After_IsInitialized_Is_True ()
+		{
+			var win1 = new Window () { Id = "win1", Width = 10, Height = 1 };
+			var view1 = new View () { Id = "view1", Width = Dim.Fill (), Height = Dim.Fill (), CanFocus = true };
+			var win2 = new Window () { Id = "win2", Y = 6, Width = 10, Height = 1 };
+			var view2 = new View () { Id = "view2", Width = Dim.Fill (), Height = Dim.Fill (), CanFocus = true };
+			win2.Add (view2);
+			win1.Add (view1, win2);
+
+			Application.Begin (win1);
 
+			Assert.True (win1.HasFocus);
+			Assert.True (view1.HasFocus);
+			Assert.False (win2.HasFocus);
+			Assert.False (view2.HasFocus);
 		}
 	}
 }

+ 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;

+ 101 - 0
UnitTests/Views/SpinnerViewTests.cs

@@ -0,0 +1,101 @@
+using System.Threading.Tasks;
+using Terminal.Gui;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace UnitTests.Views {
+	public class SpinnerViewTests {
+
+		readonly ITestOutputHelper output;
+
+		public SpinnerViewTests (ITestOutputHelper output)
+		{
+			this.output = output;
+		}
+
+		[Fact, AutoInitShutdown]
+		public void TestSpinnerView_AutoSpin()
+		{
+			var view = GetSpinnerView ();
+
+			Assert.Empty (Application.MainLoop.timeouts);
+			view.AutoSpin ();
+			Assert.NotEmpty (Application.MainLoop.timeouts);
+
+			//More calls to AutoSpin do not add more timeouts
+			Assert.Equal (1,Application.MainLoop.timeouts.Count);
+			view.AutoSpin ();
+			view.AutoSpin ();
+			view.AutoSpin ();
+			Assert.Equal (1, Application.MainLoop.timeouts.Count);
+
+			// Dispose clears timeout
+			Assert.NotEmpty (Application.MainLoop.timeouts);
+			view.Dispose ();
+			Assert.Empty (Application.MainLoop.timeouts);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void TestSpinnerView_ThrottlesAnimation ()
+		{
+			var view = GetSpinnerView ();
+
+			view.Redraw (view.Bounds);
+
+			var expected = "/";
+			TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
+
+			view.SetNeedsDisplay ();
+			view.Redraw (view.Bounds);
+
+			expected = "/";
+			TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
+
+			view.SetNeedsDisplay ();
+			view.Redraw (view.Bounds);
+
+			expected = "/";
+			TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
+
+			Task.Delay (400).Wait();
+
+			view.SetNeedsDisplay ();
+			view.Redraw (view.Bounds);
+
+			expected = "─";
+			TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
+		}
+		[Fact, AutoInitShutdown]
+		public void TestSpinnerView_NoThrottle ()
+		{
+			var view = GetSpinnerView ();
+			view.SpinDelayInMilliseconds = 0;
+
+			view.Redraw (view.Bounds);
+
+
+			var expected = @"─";
+			TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
+
+			view.SetNeedsDisplay ();
+			view.Redraw (view.Bounds);
+
+
+			expected = @"\";
+			TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
+		}
+
+		private SpinnerView GetSpinnerView ()
+		{
+			var view = new SpinnerView ();
+
+			Application.Top.Add (view);
+			Application.Begin (Application.Top);
+
+			Assert.Equal (1, view.Width);
+			Assert.Equal (1, view.Height);
+
+			return view;
+		}
+	}
+}

+ 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);