浏览代码

Fixes #2328. TextView Autocomplete triggered by cursor move.

BDisp 2 年之前
父节点
当前提交
5da7665e92

+ 45 - 22
Terminal.Gui/Core/Autocomplete/Autocomplete.cs

@@ -324,6 +324,7 @@ namespace Terminal.Gui {
 			if (IsWordChar ((char)kb.Key)) {
 				Visible = true;
 				closed = false;
+				return false;
 			}
 
 			if (kb.Key == Reopen) {
@@ -332,6 +333,9 @@ namespace Terminal.Gui {
 
 			if (closed || Suggestions.Count == 0) {
 				Visible = false;
+				if (!closed) {
+					Close ();
+				}
 				return false;
 			}
 
@@ -345,6 +349,17 @@ namespace Terminal.Gui {
 				return true;
 			}
 
+			if (kb.Key == Key.CursorLeft || kb.Key == Key.CursorRight) {
+				GenerateSuggestions (kb.Key == Key.CursorLeft ? -1 : 1);
+				if (Suggestions.Count == 0) {
+					Visible = false;
+					if (!closed) {
+						Close ();
+					}
+				}
+				return false;
+			}
+
 			if (kb.Key == SelectionKey) {
 				return Select ();
 			}
@@ -368,6 +383,9 @@ namespace Terminal.Gui {
 		public virtual bool MouseEvent (MouseEvent me, bool fromHost = false)
 		{
 			if (fromHost) {
+				if (!Visible) {
+					return false;
+				}
 				GenerateSuggestions ();
 				if (Visible && Suggestions.Count == 0) {
 					Visible = false;
@@ -444,7 +462,8 @@ namespace Terminal.Gui {
 		/// 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>
-		public virtual void GenerateSuggestions ()
+		/// <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) {
@@ -452,7 +471,7 @@ namespace Terminal.Gui {
 				return;
 			}
 
-			var currentWord = GetCurrentWord ();
+			var currentWord = GetCurrentWord (columnOffset);
 
 			if (string.IsNullOrWhiteSpace (currentWord)) {
 				ClearSuggestions ();
@@ -524,11 +543,12 @@ namespace Terminal.Gui {
 		/// <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)"/>
+		/// 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 ();
+		protected abstract string GetCurrentWord (int columnOffset = 0);
 
 		/// <summary>
 		/// <para>
@@ -536,37 +556,40 @@ namespace Terminal.Gui {
 		/// 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.</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)
+		protected virtual string IdxToWord (List<Rune> line, int idx, int columnOffset = 0)
 		{
 			StringBuilder sb = new StringBuilder ();
+			var endIdx = idx;
 
-			// do not generate suggestions if the cursor is positioned in the middle of a word
-			bool areMidWord;
-
-			if (idx == line.Count) {
-				// the cursor positioned at the very end of the line
-				areMidWord = false;
-			} else {
-				// we are in the middle of a word if the cursor is over a letter/number
-				areMidWord = IsWordChar (line [idx]);
+			// get the ending word index
+			while (endIdx < line.Count) {
+				if (IsWordChar (line [endIdx])) {
+					endIdx++;
+				} else {
+					break;
+				}
 			}
 
-			// if we are in the middle of a word then there is no way to autocomplete that word
-			if (areMidWord) {
+			// 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 (idx-- > 0) {
-
-				if (IsWordChar (line [idx])) {
-					sb.Insert (0, (char)line [idx]);
+			while (endIdx-- > 0) {
+				if (IsWordChar (line [endIdx])) {
+					sb.Insert (0, (char)line [endIdx]);
 				} else {
 					break;
 				}

+ 2 - 1
Terminal.Gui/Core/Autocomplete/IAutocomplete.cs

@@ -109,6 +109,7 @@ namespace Terminal.Gui {
 		/// 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>
-		void GenerateSuggestions ();
+		/// <param name="columnOffset">The column offset. Current (zero - default), left (negative), right (positive).</param>
+		void GenerateSuggestions (int columnOffset = 0);
 	}
 }

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

@@ -1346,12 +1346,12 @@ namespace Terminal.Gui {
 		}
 
 		/// <inheritdoc/>
-		protected override string GetCurrentWord ()
+		protected override string GetCurrentWord (int columnOffset = 0)
 		{
 			var host = (TextField)HostControl;
 			var currentLine = host.Text.ToRuneList ();
-			var cursorPosition = Math.Min (host.CursorPosition, currentLine.Count);
-			return IdxToWord (currentLine, cursorPosition);
+			var cursorPosition = Math.Min (host.CursorPosition + columnOffset, currentLine.Count);
+			return IdxToWord (currentLine, cursorPosition, columnOffset);
 		}
 
 		/// <inheritdoc/>

+ 13 - 5
Terminal.Gui/Views/TextView.cs

@@ -2447,6 +2447,10 @@ namespace Terminal.Gui {
 
 			PositionCursor ();
 
+			if (clickWithSelecting) {
+				clickWithSelecting = false;
+				return;
+			}
 			if (SelectedLength > 0)
 				return;
 
@@ -2677,8 +2681,10 @@ namespace Terminal.Gui {
 				need = true;
 			} else if ((wordWrap && leftColumn > 0) || (dSize.size + RightOffset < Frame.Width + offB.width
 				&& tSize.size + RightOffset < Frame.Width + offB.width)) {
-				leftColumn = 0;
-				need = true;
+				if (leftColumn > 0) {
+					leftColumn = 0;
+					need = true;
+				}
 			}
 
 			if (currentRow < topRow) {
@@ -4279,6 +4285,7 @@ namespace Terminal.Gui {
 		}
 
 		bool isButtonShift;
+		bool clickWithSelecting;
 
 		///<inheritdoc/>
 		public override bool MouseEvent (MouseEvent ev)
@@ -4372,6 +4379,7 @@ namespace Terminal.Gui {
 				columnTrack = currentColumn;
 			} else if (ev.Flags.HasFlag (MouseFlags.Button1Pressed)) {
 				if (shiftSelecting) {
+					clickWithSelecting = true;
 					StopSelecting ();
 				}
 				ProcessMouseClick (ev, out _);
@@ -4475,12 +4483,12 @@ namespace Terminal.Gui {
 	public class TextViewAutocomplete : Autocomplete {
 
 		///<inheritdoc/>
-		protected override string GetCurrentWord ()
+		protected override string GetCurrentWord (int columnOffset = 0)
 		{
 			var host = (TextView)HostControl;
 			var currentLine = host.GetCurrentLine ();
-			var cursorPosition = Math.Min (host.CurrentColumn, currentLine.Count);
-			return IdxToWord (currentLine, cursorPosition);
+			var cursorPosition = Math.Min (host.CurrentColumn + columnOffset, currentLine.Count);
+			return IdxToWord (currentLine, cursorPosition, columnOffset);
 		}
 
 		/// <inheritdoc/>

+ 86 - 0
UnitTests/Views/AutocompleteTests.cs

@@ -6,9 +6,16 @@ using System.Text.RegularExpressions;
 using System.Threading.Tasks;
 using Terminal.Gui;
 using Xunit;
+using Xunit.Abstractions;
 
 namespace Terminal.Gui.ViewTests {
 	public class AutocompleteTests {
+		readonly ITestOutputHelper output;
+
+		public AutocompleteTests (ITestOutputHelper output)
+		{
+			this.output = output;
+		}
 
 		[Fact]
 		public void Test_GenerateSuggestions_Simple ()
@@ -151,5 +158,84 @@ namespace Terminal.Gui.ViewTests {
 			Assert.Empty (tv.Autocomplete.Suggestions);
 			Assert.Equal (3, tv.Autocomplete.AllSuggestions.Count);
 		}
+
+		[Fact, AutoInitShutdown]
+		public void CursorLeft_CursorRight_Mouse_Button_Pressed_Does_Not_Show_Popup ()
+		{
+			var tv = new TextView () {
+				Width = 50,
+				Height = 5,
+				Text = "This a long line and against TextView."
+			};
+			tv.Autocomplete.AllSuggestions = Regex.Matches (tv.Text.ToString (), "\\w+")
+					.Select (s => s.Value)
+					.Distinct ().ToList ();
+			var top = Application.Top;
+			top.Add (tv);
+			Application.Begin (top);
+
+
+			for (int i = 0; i < 7; i++) {
+				Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
+				Application.Refresh ();
+				TestHelpers.AssertDriverContentsWithFrameAre (@"
+This a long line and against TextView.", output);
+			}
+
+			Assert.True (tv.MouseEvent (new MouseEvent () {
+				X = 6,
+				Y = 0,
+				Flags = MouseFlags.Button1Pressed
+			}));
+			Application.Refresh ();
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+This a long line and against TextView.", output);
+
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.g, new KeyModifiers ())));
+			Application.Refresh ();
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+This ag long line and against TextView.
+       against                         ", output);
+
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ())));
+			Application.Refresh ();
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+This ag long line and against TextView.
+      against                          ", output);
+
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ())));
+			Application.Refresh ();
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+This ag long line and against TextView.
+     against                           ", output);
+
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ())));
+			Application.Refresh ();
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+This ag long line and against TextView.", output);
+
+			for (int i = 0; i < 3; i++) {
+				Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
+				Application.Refresh ();
+				TestHelpers.AssertDriverContentsWithFrameAre (@"
+This ag long line and against TextView.", output);
+			}
+
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers ())));
+			Application.Refresh ();
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+This a long line and against TextView.", output);
+
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.n, new KeyModifiers ())));
+			Application.Refresh ();
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+This an long line and against TextView.
+       and                             ", output);
+
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
+			Application.Refresh ();
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+This an long line and against TextView.", output);
+		}
 	}
 }