Skip to content

Commit

Permalink
Implement inside-paren and inside-doublequote movement
Browse files Browse the repository at this point in the history
This allows for e.g. 'ci(' and 'ci"' commands.
  • Loading branch information
ascandella committed Jul 15, 2016
1 parent db168e3 commit 80a876e
Show file tree
Hide file tree
Showing 4 changed files with 238 additions and 64 deletions.
10 changes: 5 additions & 5 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <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"
: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"

Expand Down
178 changes: 119 additions & 59 deletions src/actions/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [
Expand Down Expand Up @@ -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<Position> {
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);
}
}
Expand All @@ -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<Position> {
const result = await this.execAction(position, vimState);
Expand All @@ -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<Position | IMovement> {
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<Position> {
const charToMatch = ")";
return this.nextBracket(position.getLeftThroughLineBreaks(), charToMatch, this.pairings[charToMatch], false);
return this.matcher.nextPairedChar(position.getLeftThroughLineBreaks(), charToMatch, false);
}
}

Expand All @@ -2254,7 +2314,7 @@ class MoveToUnclosedRoundBracketForward extends MoveToMatchingBracket {

public async execAction(position: Position, vimState: VimState): Promise<Position> {
const charToMatch = "(";
return this.nextBracket(position.getRightThroughLineBreaks(), charToMatch, this.pairings[charToMatch], false);
return this.matcher.nextPairedChar(position.getRightThroughLineBreaks(), charToMatch, false);
}
}

Expand All @@ -2264,7 +2324,7 @@ class MoveToUnclosedCurlyBracketBackward extends MoveToMatchingBracket {

public async execAction(position: Position, vimState: VimState): Promise<Position> {
const charToMatch = "}";
return this.nextBracket(position.getLeftThroughLineBreaks(), charToMatch, this.pairings[charToMatch], false);
return this.matcher.nextPairedChar(position.getLeftThroughLineBreaks(), charToMatch, false);
}
}

Expand All @@ -2274,7 +2334,7 @@ class MoveToUnclosedCurlyBracketForward extends MoveToMatchingBracket {

public async execAction(position: Position, vimState: VimState): Promise<Position> {
const charToMatch = "{";
return this.nextBracket(position.getRightThroughLineBreaks(), charToMatch, this.pairings[charToMatch], false);
return this.matcher.nextPairedChar(position.getRightThroughLineBreaks(), charToMatch, false);
}
}

Expand Down
79 changes: 79 additions & 0 deletions src/matching/matcher.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
35 changes: 35 additions & 0 deletions test/mode/modeNormal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down

0 comments on commit 80a876e

Please sign in to comment.