diff --git a/GitUI/SpellChecker/EditNetSpell.cs b/GitUI/SpellChecker/EditNetSpell.cs index 46edf6e5953..b69af3c4b13 100644 --- a/GitUI/SpellChecker/EditNetSpell.cs +++ b/GitUI/SpellChecker/EditNetSpell.cs @@ -209,6 +209,7 @@ protected override void OnRuntimeLoad() _customUnderlines = new SpellCheckEditControl(TextBox); TextBox.SelectionChanged += TextBox_SelectionChanged; TextBox.TextChanged += TextBoxTextChanged; + TextBox.DoubleClick += TextBox_DoubleClick; EnabledChanged += EditNetSpellEnabledChanged; @@ -706,6 +707,13 @@ private void TextBox_SelectionChanged(object sender, EventArgs e) handler?.Invoke(sender, e); } + private void TextBox_DoubleClick(object sender, EventArgs e) + { + int cursor = TextBox.GetCharIndexFromPosition(TextBox.PointToClient(MousePosition)); + (var start, var length) = _wordAtCursorExtractor.FindWord(TextBox.Text, cursor); + TextBox.Select(start, length); + } + private void ShowWatermark() { if (!ContainsFocus && string.IsNullOrEmpty(TextBox.Text) && TextBoxFont != null) diff --git a/GitUI/SpellChecker/SpellCheckerHelper.cs b/GitUI/SpellChecker/SpellCheckerHelper.cs index c2ad2dad7a5..e19ec6fd644 100644 --- a/GitUI/SpellChecker/SpellCheckerHelper.cs +++ b/GitUI/SpellChecker/SpellCheckerHelper.cs @@ -4,7 +4,7 @@ internal static class SpellCheckerHelper { public static bool IsSeparator(this char c) { - return !char.IsLetterOrDigit(c); + return !char.IsLetterOrDigit(c) && c != '_'; } } } \ No newline at end of file diff --git a/GitUI/SpellChecker/WordAtCursorExtractor.cs b/GitUI/SpellChecker/WordAtCursorExtractor.cs index 86188759d28..46b985a6977 100644 --- a/GitUI/SpellChecker/WordAtCursorExtractor.cs +++ b/GitUI/SpellChecker/WordAtCursorExtractor.cs @@ -1,4 +1,5 @@ -using System.Text; +using System; +using System.Text; namespace GitUI.SpellChecker { @@ -6,29 +7,65 @@ internal class WordAtCursorExtractor : IWordAtCursorExtractor { public string Extract(string text, int cursor) { + var startIndex = FindStartOfWord(text, cursor); + return startIndex < 0 ? string.Empty : text.Substring(startIndex, cursor - startIndex + 1); + } + + public (int start, int length) FindWord(string text, int cursor) + { + int start = Math.Min(FindStartOfWord(text, cursor), cursor); + if (start < 0) + { + return (-1, 0); + } + + int end = FindEndOfWord(text, cursor); + return (start, Math.Max(1, end - start)); + } + + public int FindStartOfWord(string text, int cursor) + { + cursor = Math.Min(cursor, text.Length - 1); if (cursor < 0) { - return string.Empty; + return -1; } - var sb = new StringBuilder(); + // do not avoid that the result can be right from the initial cursor position + #if false + if (!BelongsToStartOfWord(text, cursor)) + { + return cursor; + } + #endif - while (cursor >= 0) + while (cursor >= 0 && BelongsToStartOfWord(text, cursor)) { - if (text[cursor].IsSeparator() && !IsDot(text[cursor])) - { - break; - } + --cursor; + } - if (IsDot(text[cursor]) && !IsLeadingChar(text, cursor)) - { - break; - } + return cursor + 1; + } - sb.Insert(0, text[cursor--]); + public int FindEndOfWord(string text, int cursor) + { + cursor = Math.Min(cursor, text.Length); + if (cursor < 0) + { + return -1; + } + + while (cursor < text.Length && BelongsToWord(text[cursor])) + { + ++cursor; } - return sb.ToString(); + return cursor; + } + + private static bool BelongsToStartOfWord(string text, int cursor) + { + return IsDot(text[cursor]) ? IsLeadingChar(text, cursor) : BelongsToWord(text[cursor]); } private static bool IsDot(char c) @@ -36,6 +73,11 @@ private static bool IsDot(char c) return c == '.'; } + private static bool BelongsToWord(char c) + { + return !c.IsSeparator(); + } + private static bool IsLeadingChar(string text, int cursor) { return cursor == 0 || IsSeparatorExceptClosingBrackets(text[cursor - 1]); @@ -50,5 +92,8 @@ private static bool IsSeparatorExceptClosingBrackets(char c) internal interface IWordAtCursorExtractor { string Extract(string text, int cursor); + (int start, int length) FindWord(string text, int cursor); + int FindStartOfWord(string text, int cursor); + int FindEndOfWord(string text, int cursor); } } \ No newline at end of file diff --git a/UnitTests/GitUITests/SpellChecker/WordAtCursorExtractorTests.cs b/UnitTests/GitUITests/SpellChecker/WordAtCursorExtractorTests.cs index 436fe6d7e11..1105a58dd2a 100644 --- a/UnitTests/GitUITests/SpellChecker/WordAtCursorExtractorTests.cs +++ b/UnitTests/GitUITests/SpellChecker/WordAtCursorExtractorTests.cs @@ -20,13 +20,103 @@ public void SetUp() [TestCase("Add,git", ExpectedResult = "git")] [TestCase("Add (.git", ExpectedResult = ".git")] [TestCase("Introduce Tes", ExpectedResult = "Tes")] - [TestCase("Add).git", ExpectedResult = "git")] + [TestCase("Add).git", ExpectedResult = "git")] // test strings with a closing round bracket confuse the testcase parser a little [TestCase("[Add].git", ExpectedResult = "git")] [TestCase("Introduce .babeljs]", ExpectedResult = "")] [TestCase("func().otherFun", ExpectedResult = "otherFun")] + [TestCase("func().other_fun", ExpectedResult = "other_fun")] + [TestCase("obj._field", ExpectedResult = "_field")] + [TestCase("func(type init_", ExpectedResult = "init_")] public string ExtractsMeaningfulWord(string text) { return _wordAtCursorExtractor.Extract(text, text.Length - 1); } + + [TestCase("", -2, -1, 0, ExpectedResult = true)] + [TestCase("", -1, -1, 0, ExpectedResult = true)] + [TestCase("", 0, -1, 0, ExpectedResult = true)] + [TestCase("", 1, -1, 0, ExpectedResult = true)] + [TestCase("_obj.f", -2, -1, 0, ExpectedResult = true)] + [TestCase("_obj.f", -1, -1, 0, ExpectedResult = true)] + [TestCase("_obj.f", 0, 0, 4, ExpectedResult = true)] + [TestCase("_obj.f", 1, 0, 4, ExpectedResult = true)] + [TestCase("_obj.f", 2, 0, 4, ExpectedResult = true)] + [TestCase("_obj.f", 3, 0, 4, ExpectedResult = true)] + [TestCase("_obj.f", 4, 4, 1, ExpectedResult = true)] + [TestCase("_obj.f", 5, 5, 1, ExpectedResult = true)] + [TestCase("_obj.f", 6, 5, 1, ExpectedResult = true)] + [TestCase("_obj.f", 7, 5, 1, ExpectedResult = true)] + [TestCase("0 3", 0, 0, 1, ExpectedResult = true)] + [TestCase("0 2", 1, 1, 1, ExpectedResult = true)] + [TestCase("0 2", 2, 2, 1, ExpectedResult = true)] + [TestCase("0 2", 3, 3, 1, ExpectedResult = true)] + public bool Test_FindWord(string text, int cursor, int expectedStart, int expectedLength) + { + (var start, var length) = _wordAtCursorExtractor.FindWord(text, cursor); + return start == expectedStart && length == expectedLength; + } + + [TestCase("", -2, ExpectedResult = -1)] + [TestCase("", -1, ExpectedResult = -1)] + [TestCase("", 0, ExpectedResult = -1)] + [TestCase("", 1, ExpectedResult = -1)] + [TestCase("012", -2, ExpectedResult = -1)] + [TestCase("012", -1, ExpectedResult = -1)] + [TestCase("012", 0, ExpectedResult = 0)] + [TestCase("012", 1, ExpectedResult = 0)] + [TestCase("012", 2, ExpectedResult = 0)] + [TestCase("012", 3, ExpectedResult = 0)] + [TestCase("012", 4, ExpectedResult = 0)] + [TestCase("01A34", 5, ExpectedResult = 0)] + [TestCase("01a34", 5, ExpectedResult = 0)] + [TestCase("01_34", 5, ExpectedResult = 0)] + [TestCase("01 34", 5, ExpectedResult = 3)] + [TestCase("01.34", 5, ExpectedResult = 3)] + [TestCase("01 .4", 5, ExpectedResult = 3)] + [TestCase("01..4", 5, ExpectedResult = 3)] + [TestCase("01).4", 5, ExpectedResult = 4)] + [TestCase("01].4", 5, ExpectedResult = 4)] + [TestCase("012.4", 5, ExpectedResult = 4)] + [TestCase("012.4", 4, ExpectedResult = 4)] + [TestCase("012.4", 3, ExpectedResult = 4)] // The result is right from the initial position! + [TestCase("012/4", 3, ExpectedResult = 4)] // ditto + [TestCase("012.4", 2, ExpectedResult = 0)] + [TestCase("012.4", 1, ExpectedResult = 0)] + [TestCase("012.4", 0, ExpectedResult = 0)] + public int Test_FindStartOfWord(string text, int cursor) + { + return _wordAtCursorExtractor.FindStartOfWord(text, cursor); + } + + [TestCase("", -2, ExpectedResult = -1)] + [TestCase("", -1, ExpectedResult = -1)] + [TestCase("", 0, ExpectedResult = 0)] + [TestCase("", 1, ExpectedResult = 0)] + [TestCase("012", -2, ExpectedResult = -1)] + [TestCase("012", -1, ExpectedResult = -1)] + [TestCase("012", 0, ExpectedResult = 3)] + [TestCase("012", 1, ExpectedResult = 3)] + [TestCase("012", 2, ExpectedResult = 3)] + [TestCase("012", 3, ExpectedResult = 3)] + [TestCase("012", 4, ExpectedResult = 3)] + [TestCase("01A34", 0, ExpectedResult = 5)] + [TestCase("01a34", 0, ExpectedResult = 5)] + [TestCase("01_34", 0, ExpectedResult = 5)] + [TestCase("01 34", 0, ExpectedResult = 2)] + [TestCase("01/34", 0, ExpectedResult = 2)] + [TestCase("01/34", 1, ExpectedResult = 2)] + [TestCase("01/34", 2, ExpectedResult = 2)] + [TestCase("01/34", 3, ExpectedResult = 5)] + [TestCase("01/34", 4, ExpectedResult = 5)] + [TestCase("/12", 0, ExpectedResult = 0)] + [TestCase("/12", 1, ExpectedResult = 3)] + [TestCase("01/", 0, ExpectedResult = 2)] + [TestCase("01/", 1, ExpectedResult = 2)] + [TestCase("01/", 2, ExpectedResult = 2)] + [TestCase("01/", 3, ExpectedResult = 3)] + public int Test_FindEndOfWord(string text, int cursor) + { + return _wordAtCursorExtractor.FindEndOfWord(text, cursor); + } } } \ No newline at end of file