Skip to content

Commit

Permalink
Implement quoted text objects (#483)
Browse files Browse the repository at this point in the history
Fixes #471, #287
  • Loading branch information
ascandella authored and johnfn committed Jul 25, 2016
1 parent 32ea3a3 commit 0c18fa5
Show file tree
Hide file tree
Showing 5 changed files with 287 additions and 8 deletions.
12 changes: 6 additions & 6 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,12 +272,12 @@ Status | Command | Description
:warning: | :1234: i> | Select "inner <> block"
| :1234: at | Select "a tag block" (from <aaa> to </aaa>)
| :1234: it | Select "inner tag block" (from <aaa> to </aaa>)
| :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

Expand Down
84 changes: 84 additions & 0 deletions src/actions/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -2528,6 +2529,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<IMovement> {
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 || end < position.character) {
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<IMovement> {
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 = ["[", "("];
Expand Down
2 changes: 0 additions & 2 deletions src/matching/matcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
};
Expand Down
53 changes: 53 additions & 0 deletions src/matching/quoteMatcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
enum QuoteMatch {
None,
Opening,
Closing
}

/**
* QuoteMatcher matches quoted strings, respecting escaped quotes (\") and friends
*/
export class QuoteMatcher {
static escapeChar = "\\";

private quoteMap: QuoteMatch[] = [];

constructor(char: string, corpus: string) {
let openingQuote = false;
// 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;
}
if (corpus[i] === char) {
openingQuote = !openingQuote;
this.quoteMap[i] = openingQuote ? QuoteMatch.Opening : QuoteMatch.Closing;
}
}
}

findOpening(start: number): number {
// First, search backwards to see if we could be inside a quote
for (let i = start; i >= 0; i--) {
if (this.quoteMap[i] === QuoteMatch.Opening) {
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
for (let i = start; i <= this.quoteMap.length; i++) {
if (this.quoteMap[i]) {
return i;
}
}

return -1;
}
}
144 changes: 144 additions & 0 deletions test/mode/modeNormal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,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: "Can pick the correct open quote between two strings for 'ci\"'",
start: ['"one" |"two"'],
keysPressed: 'ci"',
end: ['"one" "|"'],
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 '],
Expand Down

0 comments on commit 0c18fa5

Please sign in to comment.