Skip to content

Commit

Permalink
Implement additional text object commands
Browse files Browse the repository at this point in the history
This allows for e.g. 'ci(', 'ca"', 'di<' commands.
  • Loading branch information
ascandella committed Jul 16, 2016
1 parent db168e3 commit 8bacbcd
Show file tree
Hide file tree
Showing 5 changed files with 255 additions and 66 deletions.
12 changes: 6 additions & 6 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,14 +268,14 @@ Status | Command | Description
| :1234: ib | Select "inner block" (from "[(" to "])")
| :1234: aB | Select "a Block" (from "[{" to "]}")
| :1234: iB | Select "inner Block" (from "[{" to "]}")
| :1234: a> | Select "a &lt;&gt; block"
| :1234: i> | Select "inner <> block"
:warning: | :1234: a> | Select "a &lt;&gt; 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
190 changes: 130 additions & 60 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,17 @@ 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;
}

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.matchesWithPercentageMotion) {
// 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 +2174,8 @@ class MoveToMatchingBracket extends BaseMovement {
return position;
}

return this.nextBracket(position, charToMatch, toFind, true);
}
return PairMatcher.nextPairedChar(position, charToMatch, true);
}

public async execActionForOperator(position: Position, vimState: VimState): Promise<Position> {
const result = await this.execAction(position, vimState);
Expand All @@ -2238,13 +2188,133 @@ class MoveToMatchingBracket extends BaseMovement {
}
}

abstract class MoveInsideCharacter extends BaseMovement {
modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine];
protected charToMatch: string;
protected includeSurrounding = false;
protected lineMatchOnly = false;

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 = position.lastIndexOf(this.charToMatch, this.lineMatchOnly);
const startPlusOne = new Position(startPos.line, startPos.character + 1);

let endPos = PairMatcher.nextPairedChar(startPlusOne, this.charToMatch, false, this.lineMatchOnly);

if (startPos === position && text[position.character] !== this.charToMatch) {
return position;
}
if (startPos.isAfter(position) || endPos.isBefore(position)) {
return position;
}

if (this.includeSurrounding) {
endPos = new Position(endPos.line, endPos.character + 1);
} else {
startPos = startPlusOne;
}
// If the closing character is the first on the line, don't swallow it.
if (endPos.character === 0) {
endPos = endPos.getLeftThroughLineBreaks();
}
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 = "\"";
lineMatchOnly = true;
}

@RegisterAction
class MoveISingleQuote extends MoveInsideCharacter {
keys = ["i", "'"];
charToMatch = "'";
lineMatchOnly = true;
}

@RegisterAction
class MoveADoubleQuote extends MoveInsideCharacter {
keys = ["a", "\""];
charToMatch = "\"";
includeSurrounding = true;
lineMatchOnly = true;
}

@RegisterAction
class MoveASingleQuote extends MoveInsideCharacter {
keys = ["a", "'"];
charToMatch = "'";
includeSurrounding = true;
lineMatchOnly = 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 PairMatcher.nextPairedChar(position.getLeftThroughLineBreaks(), charToMatch, false);
}
}

Expand All @@ -2254,7 +2324,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 PairMatcher.nextPairedChar(position.getRightThroughLineBreaks(), charToMatch, false);
}
}

Expand All @@ -2264,7 +2334,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 PairMatcher.nextPairedChar(position.getLeftThroughLineBreaks(), charToMatch, false);
}
}

Expand All @@ -2274,7 +2344,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 PairMatcher.nextPairedChar(position.getRightThroughLineBreaks(), charToMatch, false);
}
}

Expand Down
64 changes: 64 additions & 0 deletions src/matching/matcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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, matchesWithPercentageMotion?: boolean }} = {
"(" : { match: ")", nextMatchIsForward: true, matchesWithPercentageMotion: true },
"{" : { match: "}", nextMatchIsForward: true, matchesWithPercentageMotion: true },
"[" : { match: "]", nextMatchIsForward: true, matchesWithPercentageMotion: true },
")" : { match: "(", nextMatchIsForward: false, matchesWithPercentageMotion: true },
"}" : { match: "{", nextMatchIsForward: false, matchesWithPercentageMotion: true },
"]" : { match: "[", nextMatchIsForward: false, matchesWithPercentageMotion: true },
// These characters can't be used for "%"-based matching, but are still
"'" : { match: "'", nextMatchIsForward: true },
"\"": { match: "\"", nextMatchIsForward: true },
"<" : { match: ">", nextMatchIsForward: true },
};

static 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 = this.pairings[charToMatch];

let stackHeight = closed ? 0 : 1;
let matchedPosition: Position | undefined = undefined;

for (const { char, pos } of Position.IterateDocument(position, toFind.nextMatchIsForward)) {
if (currentLineOnly && position.line < pos.line) {
break;
}
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;
}
}
15 changes: 15 additions & 0 deletions src/motion/position.ts
Original file line number Diff line number Diff line change
Expand Up @@ -621,4 +621,19 @@ export class Position extends vscode.Position {

return position;
}

public lastIndexOf(char: string, currentLineOnly: boolean = false): Position {
for (const current of Position.IterateDocument(this, false)) {
if (currentLineOnly && this.line !== current.pos.line) {
// We've advanced past the current line and the caller doesn't want us to
return this;
}

if (current.char === char) {
return current.pos;
}
}

return this;
}
}
Loading

0 comments on commit 8bacbcd

Please sign in to comment.