diff --git a/PSReadLine/Movement.vi.cs b/PSReadLine/Movement.vi.cs index 011c146d..01bad815 100644 --- a/PSReadLine/Movement.vi.cs +++ b/PSReadLine/Movement.vi.cs @@ -254,7 +254,28 @@ private int ViFindBrace(int i) case ')': return ViFindBackward(i, '(', withoutPassing: ')'); default: - return i; + ReadOnlySpan parenthese = stackalloc char[] { '{', '}', '(', ')', '[', ']' }; + int nextParen = i; + // find next of any kind of paren + for (; nextParen < _buffer.Length; nextParen++) + for (int idx = 0; idx < parenthese.Length; idx++) + if (parenthese[idx] == _buffer[nextParen]) goto Outer; + + Outer: + int match = _buffer[nextParen] switch + { + // if next is opening, find forward + '{' => ViFindForward(nextParen, '}', withoutPassing: '{'), + '[' => ViFindForward(nextParen, ']', withoutPassing: '['), + '(' => ViFindForward(nextParen, ')', withoutPassing: '('), + // if next is closing, find backward + '}' => ViFindBackward(nextParen, '{', withoutPassing: '}'), + ']' => ViFindBackward(nextParen, '[', withoutPassing: ']'), + ')' => ViFindBackward(nextParen, '(', withoutPassing: ')'), + _ => nextParen + }; + + return match == nextParen ? i : match; } } diff --git a/test/MovementTest.VI.cs b/test/MovementTest.VI.cs index 3c4573eb..f41daecf 100644 --- a/test/MovementTest.VI.cs +++ b/test/MovementTest.VI.cs @@ -1,4 +1,5 @@ -using Xunit; +using System.Collections.Generic; +using Xunit; namespace Test { @@ -370,7 +371,7 @@ public void ViGlobMovement_EmptyBuffer_Defect1195() TestSetup(KeyMode.Vi); TestMustDing("", Keys( - _.Escape, "W" + _.Escape, "W" )); } @@ -423,6 +424,11 @@ public void ViCursorMovement() [SkippableFact] public void ViGotoBrace() { + // NOTE: When the input has unmatched braces, in order to avoid an + // exception caused by AcceptLineImpl waiting for incomplete input, + // the test needs to end with the Vi command "ddi" and assert that + // the result is an empty string. + TestSetup(KeyMode.Vi); Test("0[2(4{6]8)a}c", Keys( @@ -450,10 +456,60 @@ public void ViGotoBrace() CheckThat(() => AssertCursorLeftIs(4)), _.Percent, CheckThat(() => AssertCursorLeftIs(4)), - "ddi" + "ddi" // Unmatched brace )); } + // Tests when the cursor is not on any paren + foreach (var (opening, closing) in new[] { ('(', ')'), ('{', '}'), ('[', ']') }) + { + // Closing paren with backward match + string input1 = $"0{opening}2{opening}4foo{closing}"; + Test("", Keys( + input1, + CheckThat(() => AssertCursorLeftIs(9)), + _.Escape, CheckThat(() => AssertCursorLeftIs(8)), + "0ff", CheckThat(() => AssertCursorLeftIs(5)), + _.Percent, CheckThat(() => AssertCursorLeftIs(3)), + _.Percent, CheckThat(() => AssertCursorLeftIs(8)), + "ddi" // Unmatched closing brace + )); + + // Closing paren without backward match + string input2 = $"0]2)4foo{closing}"; + Test(input2, Keys( + input2, + CheckThat(() => AssertCursorLeftIs(9)), + _.Escape, CheckThat(() => AssertCursorLeftIs(8)), + "0ff", CheckThat(() => AssertCursorLeftIs(5)), + _.Percent, CheckThat(() => AssertCursorLeftIs(5)), // stay still + _.Percent, CheckThat(() => AssertCursorLeftIs(5)) + )); + + // Opening paren with forward match + string input3 = $"0{opening}2foo6{closing}"; + Test(input3, Keys( + input3, + CheckThat(() => AssertCursorLeftIs(8)), + _.Escape, CheckThat(() => AssertCursorLeftIs(7)), + "0ff", CheckThat(() => AssertCursorLeftIs(3)), + _.Percent, CheckThat(() => AssertCursorLeftIs(1)), + _.Percent, CheckThat(() => AssertCursorLeftIs(7)) + )); + + // Opening paren without forward match + string input4 = $"0)2]4foo{opening}("; + TestMustDing("", Keys( + input4, + CheckThat(() => AssertCursorLeftIs(10)), + _.Escape, CheckThat(() => AssertCursorLeftIs(9)), + "0ff", CheckThat(() => AssertCursorLeftIs(5)), + _.Percent, CheckThat(() => AssertCursorLeftIs(5)), // stay still + _.Percent, CheckThat(() => AssertCursorLeftIs(5)), + "ddi" // Unmatched brace + )); + } + // <%> with empty text buffer should work fine. Test("", Keys( _.Escape, _.Percent,