diff --git a/ROADMAP.md b/ROADMAP.md
index 0b39e0481b61..7699763b3747 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -269,13 +269,13 @@ Status | Command | Description
| :1234: aB | Select "a Block" (from "[{" to "]}")
| :1234: iB | Select "inner Block" (from "[{" to "]}")
| :1234: a> | Select "a <> block"
- | :1234: i> | Select "inner <> block"
+:warning: | :1234: i> | Select "inner <> block"
| :1234: at | Select "a tag block" (from to )
| :1234: it | Select "inner tag block" (from to )
- | :1234: a' | Select "a single quoted string"
- | :1234: i' | Select "inner single quoted string"
- | :1234: a" | Select "a double quoted string"
- | :1234: i" | Select "inner double quoted string"
+:warning: | :1234: a' | Select "a single quoted string"
+:warning: | :1234: i' | Select "inner single quoted string"
+:warning: | :1234: a" | Select "a double quoted string"
+:warning: | :1234: i" | Select "inner double quoted string"
| :1234: a` | Select "a backward quoted string"
| :1234: i` | Select "inner backward quoted string"
diff --git a/src/actions/actions.ts b/src/actions/actions.ts
index 6573ab6de28b..ac1d4c5ae86e 100644
--- a/src/actions/actions.ts
+++ b/src/actions/actions.ts
@@ -3,6 +3,7 @@ import { ModeName } from './../mode/mode';
import { TextEditor } from './../textEditor';
import { Register, RegisterMode } from './../register/register';
import { Position } from './../motion/position';
+import { PairMatcher } from './../matching/matcher';
import * as vscode from 'vscode';
const controlKeys: string[] = [
@@ -2154,68 +2155,19 @@ class MoveToMatchingBracket extends BaseMovement {
modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine];
keys = ["%"];
- pairings: { [key: string]: { match: string, nextMatchIsForward: boolean }} = {
- "(" : { match: ")", nextMatchIsForward: true },
- "{" : { match: "}", nextMatchIsForward: true },
- "[" : { match: "]", nextMatchIsForward: true },
- ")" : { match: "(", nextMatchIsForward: false },
- "}" : { match: "{", nextMatchIsForward: false },
- "]" : { match: "[", nextMatchIsForward: false },
- // "'" : { match: "'", direction: 0 },
- // "\"": { match: "\"", direction: 0 },
- };
-
- nextBracket(position: Position, charToMatch: string, toFind: { match: string, nextMatchIsForward: boolean }, closed: boolean = true) {
- /**
- * We do a fairly basic implementation that only tracks the state of the type of
- * character you're over and its pair (e.g. "[" and "]"). This is similar to
- * what Vim does.
- *
- * It can't handle strings very well - something like "|( ')' )" where | is the
- * cursor will cause it to go to the ) in the quotes, even though it should skip over it.
- *
- * PRs welcomed! (TODO)
- * Though ideally VSC implements https://github.com/Microsoft/vscode/issues/7177
- */
-
- let stackHeight = closed ? 0 : 1;
- let matchedPosition: Position | undefined = undefined;
-
- for (const { char, pos } of Position.IterateDocument(position, toFind.nextMatchIsForward)) {
- if (char === charToMatch) {
- stackHeight++;
- }
-
- if (char === this.pairings[charToMatch].match) {
- stackHeight--;
- }
-
- if (stackHeight === 0) {
- matchedPosition = pos;
-
- break;
- }
- }
-
- if (matchedPosition) {
- return matchedPosition;
- }
-
- // TODO(bell)
- return position;
- }
+ matcher = new PairMatcher();
public async execAction(position: Position, vimState: VimState): Promise {
const text = TextEditor.getLineAt(position).text;
const charToMatch = text[position.character];
- const toFind = this.pairings[charToMatch];
+ const toFind = PairMatcher.pairings[charToMatch];
- if (!toFind) {
+ if (!toFind || toFind.limited) {
// If we're not on a match, go right until we find a
// pairable character or hit the end of line.
for (let i = position.character; i < text.length; i++) {
- if (this.pairings[text[i]]) {
+ if (PairMatcher.pairings[text[i]]) {
return new Position(position.line, i);
}
}
@@ -2224,8 +2176,8 @@ class MoveToMatchingBracket extends BaseMovement {
return position;
}
- return this.nextBracket(position, charToMatch, toFind, true);
-}
+ return this.matcher.nextPairedChar(position, charToMatch, true);
+ }
public async execActionForOperator(position: Position, vimState: VimState): Promise {
const result = await this.execAction(position, vimState);
@@ -2238,13 +2190,121 @@ class MoveToMatchingBracket extends BaseMovement {
}
}
+abstract class MoveInsideCharacter extends BaseMovement {
+ modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine];
+ protected charToMatch: string;
+ protected includeSurrounding = false;
+ protected matcher = new PairMatcher();
+
+ public async execAction(position: Position, vimState: VimState): Promise {
+ const text = TextEditor.getLineAt(position).text;
+
+ // First, search backwards for the opening character of the sequence
+ let startPos = this.matcher.previousChar(position, this.charToMatch);
+ const startPlusOne = new Position(startPos.line, startPos.character + 1);
+
+ let endPos = this.matcher.nextPairedChar(startPlusOne, this.charToMatch, false);
+ if (startPos.isAfter(position) || endPos.isBefore(position)) {
+ return position;
+ }
+
+ if (this.includeSurrounding) {
+ endPos = new Position(endPos.line, endPos.character + 1);
+ } else {
+ startPos = startPlusOne;
+ }
+ return {
+ start : startPos,
+ stop : endPos,
+ };
+ }
+}
+
+@RegisterAction
+class MoveIParentheses extends MoveInsideCharacter {
+ keys = ["i", "("];
+ charToMatch = "(";
+}
+
+@RegisterAction
+class MoveIClosingParentheses extends MoveInsideCharacter {
+ keys = ["i", ")"];
+ charToMatch = "(";
+}
+
+@RegisterAction
+class MoveAParentheses extends MoveInsideCharacter {
+ keys = ["a", "("];
+ charToMatch = "(";
+ includeSurrounding = true;
+}
+
+@RegisterAction
+class MoveAClosingParentheses extends MoveInsideCharacter {
+ keys = ["a", ")"];
+ charToMatch = "(";
+ includeSurrounding = true;
+}
+
+@RegisterAction
+class MoveICaret extends MoveInsideCharacter {
+ keys = ["i", "<"];
+ charToMatch = "<";
+}
+
+@RegisterAction
+class MoveIClosingCaret extends MoveInsideCharacter {
+ keys = ["i", ">"];
+ charToMatch = "<";
+}
+
+@RegisterAction
+class MoveACaret extends MoveInsideCharacter {
+ keys = ["a", "<"];
+ charToMatch = "<";
+ includeSurrounding = true;
+}
+
+@RegisterAction
+class MoveAClosingCaret extends MoveInsideCharacter {
+ keys = ["a", ">"];
+ charToMatch = "<";
+ includeSurrounding = true;
+}
+
+@RegisterAction
+class MoveIDoubleQuote extends MoveInsideCharacter {
+ keys = ["i", "\""];
+ charToMatch = "\"";
+}
+
+@RegisterAction
+class MoveISingleQuote extends MoveInsideCharacter {
+ keys = ["i", "'"];
+ charToMatch = "'";
+}
+
+@RegisterAction
+class MoveADoubleQuote extends MoveInsideCharacter {
+ keys = ["a", "\""];
+ charToMatch = "\"";
+ includeSurrounding = true;
+}
+
+@RegisterAction
+class MoveASingleQuote extends MoveInsideCharacter {
+ keys = ["a", "'"];
+ charToMatch = "'";
+ includeSurrounding = true;
+}
+
@RegisterAction
class MoveToUnclosedRoundBracketBackward extends MoveToMatchingBracket {
keys = ["[", "("];
public async execAction(position: Position, vimState: VimState): Promise {
const charToMatch = ")";
- return this.nextBracket(position.getLeftThroughLineBreaks(), charToMatch, this.pairings[charToMatch], false);
+ return this.matcher.nextPairedChar(position.getLeftThroughLineBreaks(), charToMatch, false);
}
}
@@ -2254,7 +2314,7 @@ class MoveToUnclosedRoundBracketForward extends MoveToMatchingBracket {
public async execAction(position: Position, vimState: VimState): Promise {
const charToMatch = "(";
- return this.nextBracket(position.getRightThroughLineBreaks(), charToMatch, this.pairings[charToMatch], false);
+ return this.matcher.nextPairedChar(position.getRightThroughLineBreaks(), charToMatch, false);
}
}
@@ -2264,7 +2324,7 @@ class MoveToUnclosedCurlyBracketBackward extends MoveToMatchingBracket {
public async execAction(position: Position, vimState: VimState): Promise {
const charToMatch = "}";
- return this.nextBracket(position.getLeftThroughLineBreaks(), charToMatch, this.pairings[charToMatch], false);
+ return this.matcher.nextPairedChar(position.getLeftThroughLineBreaks(), charToMatch, false);
}
}
@@ -2274,7 +2334,7 @@ class MoveToUnclosedCurlyBracketForward extends MoveToMatchingBracket {
public async execAction(position: Position, vimState: VimState): Promise {
const charToMatch = "{";
- return this.nextBracket(position.getRightThroughLineBreaks(), charToMatch, this.pairings[charToMatch], false);
+ return this.matcher.nextPairedChar(position.getRightThroughLineBreaks(), charToMatch, false);
}
}
diff --git a/src/matching/matcher.ts b/src/matching/matcher.ts
new file mode 100644
index 000000000000..9d61ba2e84c3
--- /dev/null
+++ b/src/matching/matcher.ts
@@ -0,0 +1,79 @@
+import { Position } from './../motion/position';
+
+/**
+ * PairMatcher finds the position matching the given character, respecting nested
+ * instances of the pair.
+ */
+export class PairMatcher {
+ static pairings: { [key: string]: { match: string, nextMatchIsForward: boolean, limited?: boolean }} = {
+ "(" : { match: ")", nextMatchIsForward: true },
+ "{" : { match: "}", nextMatchIsForward: true },
+ "[" : { match: "]", nextMatchIsForward: true },
+ ")" : { match: "(", nextMatchIsForward: false },
+ "}" : { match: "{", nextMatchIsForward: false },
+ "]" : { match: "[", nextMatchIsForward: false },
+ // These characters can't be used for "%"-based matching, but are still
+ // useful for text objects. TODO limited is a terrible name.
+ "'" : { match: "'", nextMatchIsForward: true, limited: true },
+ "\"": { match: "\"", nextMatchIsForward: true, limited: true },
+ "<" : { match: ">", nextMatchIsForward: true, limited: true },
+ };
+
+ previousChar(position: Position, charToMatch: string, currentLineOnly: boolean = false): Position {
+ // TODO handle currentLineOnly
+ for (const { char, pos } of Position.IterateDocument(position, false)) {
+ if (currentLineOnly && position.line != pos.line) {
+ // We've advanced past the current line and the caller doesn't want us to
+ return position;
+ }
+
+ if (char === charToMatch) {
+ return pos;
+ }
+ }
+
+ return position;
+ }
+
+ nextPairedChar(position: Position, charToMatch: string, closed: boolean = true, currentLineOnly: boolean = false): Position {
+ /**
+ * We do a fairly basic implementation that only tracks the state of the type of
+ * character you're over and its pair (e.g. "[" and "]"). This is similar to
+ * what Vim does.
+ *
+ * It can't handle strings very well - something like "|( ')' )" where | is the
+ * cursor will cause it to go to the ) in the quotes, even though it should skip over it.
+ *
+ * PRs welcomed! (TODO)
+ * Though ideally VSC implements https://github.com/Microsoft/vscode/issues/7177
+ */
+ const toFind = PairMatcher.pairings[charToMatch];
+
+ let stackHeight = closed ? 0 : 1;
+ let matchedPosition: Position | undefined = undefined;
+ // TODO respect currentLineOnly
+
+ for (const { char, pos } of Position.IterateDocument(position, toFind.nextMatchIsForward)) {
+ if (char === charToMatch && charToMatch !== toFind.match) {
+ stackHeight++;
+ }
+
+ if (char === toFind.match) {
+ stackHeight--;
+ }
+
+ if (stackHeight === 0) {
+ matchedPosition = pos;
+
+ break;
+ }
+ }
+
+ if (matchedPosition) {
+ return matchedPosition;
+ }
+
+ // TODO(bell)
+ return position;
+ }
+}
\ No newline at end of file
diff --git a/test/mode/modeNormal.test.ts b/test/mode/modeNormal.test.ts
index e9c9280e03e9..3751957d3553 100644
--- a/test/mode/modeNormal.test.ts
+++ b/test/mode/modeNormal.test.ts
@@ -219,6 +219,41 @@ suite("Mode Normal", () => {
endMode: ModeName.Insert
});
+ newTest({
+ title: "Can handle 'ci(' on first parentheses",
+ start: ['print(|"hello")'],
+ keysPressed: 'ci(',
+ end: ['print(|)']
+ });
+
+ newTest({
+ title: "Can handle 'ci(' with nested parentheses",
+ start: ['call|(() => 5)'],
+ keysPressed: 'ci(',
+ end: ['call(|)']
+ });
+
+ newTest({
+ title: "Can handle 'ci\"' inside a string",
+ start: ['"hel|lo" world'],
+ keysPressed: 'ci"',
+ end: ['"|" world']
+ });
+
+ newTest({
+ title: "Can handle \"ci'\" inside a string",
+ start: ['\'on|e\' two'],
+ keysPressed: 'ci\'',
+ end: ['\'|\' two']
+ });
+
+ newTest({
+ title: "Can handle 'ca(' spanning multiple lines",
+ start: ['call(', ' |arg1)'],
+ keysPressed: 'ca(',
+ end: ['call|']
+ });
+
newTest({
title: "Can handle 'df'",
start: ['aext tex|t'],