diff --git a/ROADMAP.md b/ROADMAP.md
index f90a49a52ebb..d73d08525e3d 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -272,12 +272,12 @@ Status | Command | Description
: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"
- | :1234: a` | Select "a backward quoted string"
- | :1234: i` | Select "inner backward 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"
+:warning: | :1234: a` | Select "a backward quoted string"
+:warning: | :1234: i` | Select "inner backward quoted string"
## Repeating commands
diff --git a/src/actions/actions.ts b/src/actions/actions.ts
index 27497a9b82ac..f79020ed4484 100644
--- a/src/actions/actions.ts
+++ b/src/actions/actions.ts
@@ -4,6 +4,7 @@ import { TextEditor } from './../textEditor';
import { Register, RegisterMode } from './../register/register';
import { Position } from './../motion/position';
import { PairMatcher } from './../matching/matcher';
+import { QuoteMatcher } from './../matching/quoteMatcher';
import { Tab, TabCommand } from './../cmd_line/commands/tab';
import * as vscode from 'vscode';
@@ -2521,6 +2522,89 @@ class MoveAClosingSquareBracket extends MoveInsideCharacter {
includeSurrounding = true;
}
+abstract class MoveQuoteMatch extends BaseMovement {
+ modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine];
+ protected charToMatch: string;
+ protected includeSurrounding = false;
+
+ public async execAction(position: Position, vimState: VimState): Promise {
+ const text = TextEditor.getLineAt(position).text;
+ const quoteMatcher = new QuoteMatcher(this.charToMatch, text);
+ const start = quoteMatcher.findOpening(position.character);
+ const end = quoteMatcher.findClosing(start + 1);
+
+ if (start === -1 || end === -1 || end === start) {
+ return {
+ start: position,
+ stop: position,
+ failed: true
+ };
+ }
+
+ let startPos = new Position(position.line, start);
+ let endPos = new Position(position.line, end);
+ if (!this.includeSurrounding) {
+ startPos = startPos.getRight();
+ endPos = endPos.getLeft();
+ }
+
+ return {
+ start: startPos,
+ stop: endPos
+ };
+ }
+
+ public async execActionForOperator(position: Position, vimState: VimState): Promise {
+ const res = await this.execAction(position, vimState);
+
+ res.stop = res.stop.getRight();
+
+ return res;
+ }
+}
+
+@RegisterAction
+class MoveInsideSingleQuotes extends MoveQuoteMatch {
+ keys = ["i", "'"];
+ charToMatch = "'";
+ includeSurrounding = false;
+}
+
+@RegisterAction
+class MoveASingleQuotes extends MoveQuoteMatch {
+ keys = ["a", "'"];
+ charToMatch = "'";
+ includeSurrounding = true;
+}
+
+@RegisterAction
+class MoveInsideDoubleQuotes extends MoveQuoteMatch {
+ keys = ["i", "\""];
+ charToMatch = "\"";
+ includeSurrounding = false;
+}
+
+@RegisterAction
+class MoveADoubleQuotes extends MoveQuoteMatch {
+ keys = ["a", "\""];
+ charToMatch = "\"";
+ includeSurrounding = true;
+}
+
+@RegisterAction
+class MoveInsideBacktick extends MoveQuoteMatch {
+ keys = ["i", "`"];
+ charToMatch = "`";
+ includeSurrounding = false;
+}
+
+@RegisterAction
+class MoveABacktick extends MoveQuoteMatch {
+ keys = ["a", "`"];
+ charToMatch = "`";
+ includeSurrounding = true;
+}
+
@RegisterAction
class MoveToUnclosedRoundBracketBackward extends MoveToMatchingBracket {
keys = ["[", "("];
diff --git a/src/matching/matcher.ts b/src/matching/matcher.ts
index 11edbc6d190f..82dd1aa27584 100644
--- a/src/matching/matcher.ts
+++ b/src/matching/matcher.ts
@@ -14,8 +14,6 @@ export class PairMatcher {
"]" : { match: "[", nextMatchIsForward: false, matchesWithPercentageMotion: true },
// These characters can't be used for "%"-based matching, but are still
// useful for text objects.
- // "'" : { match: "'", nextMatchIsForward: true },
- // "\"": { match: "\"", nextMatchIsForward: true },
"<" : { match: ">", nextMatchIsForward: true },
">" : { match: "<", nextMatchIsForward: false },
};
diff --git a/src/matching/quoteMatcher.ts b/src/matching/quoteMatcher.ts
new file mode 100644
index 000000000000..df44df32cdd1
--- /dev/null
+++ b/src/matching/quoteMatcher.ts
@@ -0,0 +1,37 @@
+/**
+ * QuoteMatcher matches quoted strings, respecting escaped quotes (\") and friends
+ */
+export class QuoteMatcher {
+ static escapeChar = "\\";
+
+ private quoteMap: boolean[] = [];
+
+ constructor(char: string, corpus: string) {
+ // Loop over corpus, marking quotes and respecting escape characters.
+ for (let i = 0; i < corpus.length; i++) {
+ if (corpus[i] === QuoteMatcher.escapeChar) {
+ i += 1;
+ continue;
+ }
+ this.quoteMap[i] = corpus[i] === char;
+ }
+ }
+
+ findOpening(start: number): number {
+ // First, search backwards to see if we could be inside a quote
+ for (let i = start - 1; i >= 0; i--) {
+ if (this.quoteMap[i]) {
+ return i;
+ }
+ }
+
+ // Didn't find one behind us, the string may start ahead of us. This happens
+ // to be the same logic we use to search forwards.
+ return this.findClosing(start);
+ }
+
+ findClosing(start: number): number {
+ // Search forwards from start, looking for a non-escaped char
+ return this.quoteMap.indexOf(true, start);
+ }
+}
\ No newline at end of file
diff --git a/test/mode/modeNormal.test.ts b/test/mode/modeNormal.test.ts
index 1402507d306e..d908c00260ea 100644
--- a/test/mode/modeNormal.test.ts
+++ b/test/mode/modeNormal.test.ts
@@ -328,6 +328,150 @@ suite("Mode Normal", () => {
endMode: ModeName.Insert
});
+ newTest({
+ title: "Can handle 'ci\'' on first quote",
+ start: ["|'one'"],
+ keysPressed: "ci'",
+ end: ["'|'"],
+ endMode: ModeName.Insert
+ });
+
+ newTest({
+ title: "Can handle 'ci\'' inside quoted string",
+ start: ["'o|ne'"],
+ keysPressed: "ci'",
+ end: ["'|'"],
+ endMode: ModeName.Insert
+ });
+
+ newTest({
+ title: "Can handle 'ci\'' on closing quote",
+ start: ["'one|'"],
+ keysPressed: "ci'",
+ end: ["'|'"],
+ endMode: ModeName.Insert
+ });
+
+ newTest({
+ title: "Can handle 'ci\'' when string is ahead",
+ start: ["on|e 'two'"],
+ keysPressed: "ci'",
+ end: ["one '|'"],
+ endMode: ModeName.Insert
+ });
+
+ newTest({
+ title: "Can handle 'ci\"' on opening quote",
+ start: ['|"one"'],
+ keysPressed: 'ci"',
+ end: ['"|"'],
+ endMode: ModeName.Insert
+ });
+
+ newTest({
+ title: "Can handle 'ci\"' starting behind the quoted word",
+ start: ['|one "two"'],
+ keysPressed: 'ci"',
+ end: ['one "|"'],
+ endMode: ModeName.Insert
+ });
+
+ newTest({
+ title: "Can handle 'ca\"' starting behind the quoted word",
+ start: ['|one "two"'],
+ keysPressed: 'ca"',
+ end: ['one |'],
+ endMode: ModeName.Insert
+ });
+
+ newTest({
+ title: "Can handle 'ca\"' starting on the opening quote",
+ start: ['one |"two"'],
+ keysPressed: 'ca"',
+ end: ['one |'],
+ endMode: ModeName.Insert
+ });
+
+ newTest({
+ title: "Can handle 'ci\"' with escaped quotes",
+ start: ['"one \\"tw|o\\""'],
+ keysPressed: 'ci"',
+ end: ['"|"'],
+ endMode: ModeName.Insert
+ });
+
+ newTest({
+ title: "Can handle 'ci\"' with a single escaped quote",
+ start: ['|"one \\" two"'],
+ keysPressed: 'ci"',
+ end: ['"|"'],
+ endMode: ModeName.Insert
+ });
+
+ newTest({
+ title: "Can handle 'ci\"' with a single escaped quote behind",
+ start: ['one "two \\" |three"'],
+ keysPressed: 'ci"',
+ end: ['one "|"'],
+ endMode: ModeName.Insert
+ });
+
+ newTest({
+ title: "Can handle 'ci\"' with an escaped backslash",
+ start: ['one "tw|o \\\\three"'],
+ keysPressed: 'ci"',
+ end: ['one "|"'],
+ endMode: ModeName.Insert
+ });
+
+ newTest({
+ title: "Can handle 'ci\"' with an escaped backslash on closing quote",
+ start: ['"\\\\|"'],
+ keysPressed: 'ci"',
+ end: ['"|"'],
+ endMode: ModeName.Insert
+ });
+
+ newTest({
+ title: "Can handle 'ca\"' starting on the closing quote",
+ start: ['one "two|"'],
+ keysPressed: 'ca"',
+ end: ['one |'],
+ endMode: ModeName.Insert
+ });
+
+ newTest({
+ title: "Can handle 'ci\"' with complex escape sequences",
+ start: ['"two|\\\\\\""'],
+ keysPressed: 'ci"',
+ end: ['"|"'],
+ endMode: ModeName.Insert
+ });
+
+ newTest({
+ title: "Emulates vim's behavior sandwiched between double quoted strings for 'ci\"'",
+ start: ['"one" |"two"'],
+ keysPressed: 'ci"',
+ end: ['"one"|"two"'],
+ endMode: ModeName.Insert
+ });
+
+ newTest({
+ title: "will fail when ca\" ahead of quoted string",
+ start: ['"one" |two'],
+ keysPressed: 'ca"',
+ end: ['"one" |two'],
+ endMode: ModeName.Normal
+ });
+
+ newTest({
+ title: "Can handle 'ca`' inside word",
+ start: ['one `t|wo`'],
+ keysPressed: 'ca`',
+ end: ['one |'],
+ endMode: ModeName.Insert
+ });
+
newTest({
title: "Can handle 'daw' on word with cursor inside spaces",
start: ['one two | three, four '],