From 5da7665e921691405b305d367a22f9d5b678c75e Mon Sep 17 00:00:00 2001 From: BDisp Date: Wed, 8 Feb 2023 00:12:52 +0000 Subject: [PATCH] Fixes #2328. TextView Autocomplete triggered by cursor move. --- .../Core/Autocomplete/Autocomplete.cs | 67 ++++++++++----- .../Core/Autocomplete/IAutocomplete.cs | 3 +- Terminal.Gui/Views/TextField.cs | 6 +- Terminal.Gui/Views/TextView.cs | 18 ++-- UnitTests/Views/AutocompleteTests.cs | 86 +++++++++++++++++++ 5 files changed, 149 insertions(+), 31 deletions(-) diff --git a/Terminal.Gui/Core/Autocomplete/Autocomplete.cs b/Terminal.Gui/Core/Autocomplete/Autocomplete.cs index f351b8424..61ccc01ea 100644 --- a/Terminal.Gui/Core/Autocomplete/Autocomplete.cs +++ b/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 with all strings in that /// match with the current cursor position/text in the /// - public virtual void GenerateSuggestions () + /// The column offset. + 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 { /// /// Returns the currently selected word from the . /// - /// When overriding this method views can make use of + /// When overriding this method views can make use of /// /// + /// The column offset. /// - protected abstract string GetCurrentWord (); + protected abstract string GetCurrentWord (int columnOffset = 0); /// /// @@ -536,37 +556,40 @@ namespace Terminal.Gui { /// or null. Also returns null if the is positioned in the middle of a word. /// /// - /// 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 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 to indicate if search the word at left (negative), + /// at right (positive) or at the current column (zero) which is the default. + /// /// /// /// + /// /// - protected virtual string IdxToWord (List line, int idx) + protected virtual string IdxToWord (List 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; } diff --git a/Terminal.Gui/Core/Autocomplete/IAutocomplete.cs b/Terminal.Gui/Core/Autocomplete/IAutocomplete.cs index 32e7046e7..2e3194eb3 100644 --- a/Terminal.Gui/Core/Autocomplete/IAutocomplete.cs +++ b/Terminal.Gui/Core/Autocomplete/IAutocomplete.cs @@ -109,6 +109,7 @@ namespace Terminal.Gui { /// Populates with all strings in that /// match with the current cursor position/text in the . /// - void GenerateSuggestions (); + /// The column offset. Current (zero - default), left (negative), right (positive). + void GenerateSuggestions (int columnOffset = 0); } } diff --git a/Terminal.Gui/Views/TextField.cs b/Terminal.Gui/Views/TextField.cs index 14fc56394..07389fb4f 100644 --- a/Terminal.Gui/Views/TextField.cs +++ b/Terminal.Gui/Views/TextField.cs @@ -1346,12 +1346,12 @@ namespace Terminal.Gui { } /// - 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); } /// diff --git a/Terminal.Gui/Views/TextView.cs b/Terminal.Gui/Views/TextView.cs index dd609f12c..7cb797b11 100644 --- a/Terminal.Gui/Views/TextView.cs +++ b/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; /// 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 { /// - 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); } /// diff --git a/UnitTests/Views/AutocompleteTests.cs b/UnitTests/Views/AutocompleteTests.cs index 51dd0076c..dca1fd81c 100644 --- a/UnitTests/Views/AutocompleteTests.cs +++ b/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); + } } } \ No newline at end of file