Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 45 additions & 22 deletions Terminal.Gui/Core/Autocomplete/Autocomplete.cs
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ public virtual bool ProcessKey (KeyEvent kb)
if (IsWordChar ((char)kb.Key)) {
Visible = true;
closed = false;
return false;
}

if (kb.Key == Reopen) {
Expand All @@ -332,6 +333,9 @@ public virtual bool ProcessKey (KeyEvent kb)

if (closed || Suggestions.Count == 0) {
Visible = false;
if (!closed) {
Close ();
}
return false;
}

Expand All @@ -345,6 +349,17 @@ public virtual bool ProcessKey (KeyEvent kb)
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 ();
}
Expand All @@ -368,6 +383,9 @@ public virtual bool ProcessKey (KeyEvent kb)
public virtual bool MouseEvent (MouseEvent me, bool fromHost = false)
{
if (fromHost) {
if (!Visible) {
return false;
}
GenerateSuggestions ();
if (Visible && Suggestions.Count == 0) {
Visible = false;
Expand Down Expand Up @@ -444,15 +462,16 @@ public virtual void ClearSuggestions ()
/// 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) {
ClearSuggestions ();
return;
}

var currentWord = GetCurrentWord ();
var currentWord = GetCurrentWord (columnOffset);

if (string.IsNullOrWhiteSpace (currentWord)) {
ClearSuggestions ();
Expand Down Expand Up @@ -524,49 +543,53 @@ protected virtual bool InsertSelection (string accepted)
/// <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>
/// 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.</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;
}
Expand Down
3 changes: 2 additions & 1 deletion Terminal.Gui/Core/Autocomplete/IAutocomplete.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ public interface IAutocomplete {
/// 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);
}
}
6 changes: 3 additions & 3 deletions Terminal.Gui/Views/TextField.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1346,12 +1346,12 @@ protected override void DeleteTextBackwards ()
}

/// <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/>
Expand Down
18 changes: 13 additions & 5 deletions Terminal.Gui/Views/TextView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2447,6 +2447,10 @@ public override void Redraw (Rect bounds)

PositionCursor ();

if (clickWithSelecting) {
clickWithSelecting = false;
return;
}
if (SelectedLength > 0)
return;

Expand Down Expand Up @@ -2677,8 +2681,10 @@ void Adjust ()
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) {
Expand Down Expand Up @@ -4279,6 +4285,7 @@ void ProcMovePrev (ref int nCol, ref int nRow, Rune nRune)
}

bool isButtonShift;
bool clickWithSelecting;

///<inheritdoc/>
public override bool MouseEvent (MouseEvent ev)
Expand Down Expand Up @@ -4372,6 +4379,7 @@ public override bool MouseEvent (MouseEvent ev)
columnTrack = currentColumn;
} else if (ev.Flags.HasFlag (MouseFlags.Button1Pressed)) {
if (shiftSelecting) {
clickWithSelecting = true;
StopSelecting ();
}
ProcessMouseClick (ev, out _);
Expand Down Expand Up @@ -4475,12 +4483,12 @@ public void ClearHistoryChanges ()
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/>
Expand Down
86 changes: 86 additions & 0 deletions UnitTests/Views/AutocompleteTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,16 @@
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 ()
Expand Down Expand Up @@ -151,5 +158,84 @@ public void KeyBindings_Command ()
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);
}
}
}