Skip to content

Commit

Permalink
Merge pull request VSCodeVim#1550 from mikew/indent-object
Browse files Browse the repository at this point in the history
Add support for indent objects
  • Loading branch information
johnfn authored Apr 27, 2017
2 parents 7074c4d + aec8dea commit c57d370
Show file tree
Hide file tree
Showing 4 changed files with 304 additions and 0 deletions.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ Donations help convince me to work on this project rather than my other (non-ope
* [vim-easymotion](#vim-easymotion)
* [vim-surround](#vim-surround)
* [vim-commentary](#vim-commentary)
* [vim-indent-object](#vim-indent-object)
* [VSCodeVim tricks](#vscodevim-tricks)
* [F.A.Q / Troubleshooting](#faq)
* [Contributing](#contributing)
Expand Down Expand Up @@ -447,6 +448,18 @@ If you are use to using vim-commentary you are probably use to using `gc` instea
],
```

### vim-indent-object

Indent Objects in VSCodeVim are identical to [michaeljsmith/vim-indent-object](https://github.com/michaeljsmith/vim-indent-object) and allow you to treat blocks of code at the current indentation level as text objects. This is very useful in languages that don't use braces around statements, like Python.

Provided there is a new line between the opening and closing braces / tag, it can be considered an agnostic `cib`/`ci{`/`ci[`/`cit`.

Command | Description
---|--------
`<operator>ii`|This indentation level
`<operator>ai`|This indentation level and the line above (think `if` statements in Python)
`<operator>aI`|This indentation level, the line above, and the line after (think `if` statements in C/C++/Java/etc)

## VSCodeVim tricks!

**Awesome Features You Might Not Know About**
Expand Down
110 changes: 110 additions & 0 deletions src/actions/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6315,6 +6315,116 @@ class MoveAroundTag extends MoveTagMatch {
includeTag = true;
}

abstract class IndentObjectMatch extends TextObjectMovement {
setsDesiredColumnToEOL = true;

protected includeLineAbove = false;
protected includeLineBelow = false;

public async execAction(position: Position, vimState: VimState): Promise<IMovement> {
const isChangeOperator = vimState.recordedState.operator instanceof ChangeOperator;
const firstValidLineNumber = IndentObjectMatch.findFirstValidLine(position);
const firstValidLine = TextEditor.getLineAt(new Position(firstValidLineNumber, 0));
const cursorIndent = firstValidLine.firstNonWhitespaceCharacterIndex;

// let startLineNumber = findRangeStart(firstValidLineNumber, cursorIndent);
let startLineNumber = IndentObjectMatch.findRangeStartOrEnd(firstValidLineNumber, cursorIndent, -1);
let endLineNumber = IndentObjectMatch.findRangeStartOrEnd(firstValidLineNumber, cursorIndent, 1);

// Adjust the start line as needed.
if (this.includeLineAbove) {
startLineNumber -= 1;
}
// Check for OOB.
if (startLineNumber < 0) {
startLineNumber = 0;
}

// Adjust the end line as needed.
if (this.includeLineBelow) {
endLineNumber += 1;
}
// Check for OOB.
if (endLineNumber > TextEditor.getLineCount() - 1) {
endLineNumber = TextEditor.getLineCount() - 1;
}

// If initiated by a change operation, adjust the cursor to the indent level
// of the block.
let startCharacter = 0;
if (isChangeOperator) {
startCharacter = TextEditor.getLineAt(new Position(startLineNumber, 0)).firstNonWhitespaceCharacterIndex;
}
// TextEditor.getLineMaxColumn throws when given line 0, which we don't
// care about here since it just means this text object wouldn't work on a
// single-line document.
const endCharacter = TextEditor.readLineAt(endLineNumber).length;

return {
start: new Position(startLineNumber, startCharacter),
stop: new Position(endLineNumber, endCharacter),
};
}

/**
* Searches up from the cursor for the first non-empty line.
*/
public static findFirstValidLine(cursorPosition: Position): number {
for (let i = cursorPosition.line; i >= 0; i--) {
const line = TextEditor.getLineAt(new Position(i, 0));

if (!line.isEmptyOrWhitespace) {
return i;
}
}

return cursorPosition.line;
}

/**
* Searches up or down from a line finding the first with a lower indent level.
*/
public static findRangeStartOrEnd (startIndex: number, cursorIndent: number, step: -1 | 1): number {
let i = startIndex;
let ret = startIndex;
const end = step === 1
? TextEditor.getLineCount()
: -1;

for (; i !== end; i += step) {
const line = TextEditor.getLineAt(new Position(i, 0));
const isLineEmpty = line.isEmptyOrWhitespace;
const lineIndent = line.firstNonWhitespaceCharacterIndex;

if (lineIndent < cursorIndent && !isLineEmpty) {
break;
}

ret = i;
}

return ret;
}
}

@RegisterAction
class InsideIndentObject extends IndentObjectMatch {
keys = ["i", "i"];
}

@RegisterAction
class InsideIndentObjectAbove extends IndentObjectMatch {
keys = ["a", "i"];
includeLineAbove = true;
}

@RegisterAction
class InsideIndentObjectBoth extends IndentObjectMatch {
keys = ["a", "I"];
includeLineAbove = true;
includeLineBelow = true;
}

@RegisterAction
class ActionTriggerHover extends BaseCommand {
modes = [ModeName.Normal];
Expand Down
97 changes: 97 additions & 0 deletions test/mode/modeNormal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1509,4 +1509,101 @@ suite("Mode Normal", () => {
keysPressed: 'cc',
end: ["|"],
});

newTest({
title: "Can do cai",
start: [
'if foo > 3:',
' log("foo is big")|',
' foo = 3',
'do_something_else()',
],
keysPressed: "cai",
end: [
'|',
'do_something_else()',
],
endMode: ModeName.Insert
});

newTest({
title: "Can do cii",
start: [
'if foo > 3:',
'\tlog("foo is big")',
'\tfoo = 3',
'|',
'do_something_else()',
],
keysPressed: "cii",
end: [
'if foo > 3:',
'\t|',
'do_something_else()',
],
endMode: ModeName.Insert
});

newTest({
title: "Can do caI",
start: [
'if foo > 3:',
' log("foo is big")|',
' foo = 3',
'do_something_else()',
],
keysPressed: "caI",
end: [
'|',
],
endMode: ModeName.Insert
});

newTest({
title: "Can do dai",
start: [
'if foo > 3:',
' log("foo is big")|',
' foo = 3',
'do_something_else()',
],
keysPressed: "dai",
end: [
'|',
'do_something_else()',
],
endMode: ModeName.Normal
});

newTest({
title: "Can do dii",
start: [
'if foo > 3:',
' log("foo is big")',
' foo = 3',
'|',
'do_something_else()',
],
keysPressed: "dii",
end: [
'if foo > 3:',
'|do_something_else()',
],
endMode: ModeName.Normal
});

newTest({
title: "Can do daI",
start: [
'if foo > 3:',
' log("foo is big")|',
' foo = 3',
'do_something_else()',
],
keysPressed: "daI",
end: [
'|',
],
endMode: ModeName.Normal
});
});
84 changes: 84 additions & 0 deletions test/mode/modeVisual.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -665,4 +665,88 @@ suite("Mode Visual", () => {
endMode: ModeName.Normal
});
});

suite("handles indent blocks in visual mode", () => {
newTest({
title: "Can do vai",
start: [
'if foo > 3:',
' log("foo is big")|',
' foo = 3',
'do_something_else()',
],
keysPressed: "vaid",
end: [
'|do_something_else()',
],
endMode: ModeName.Normal
});

newTest({
title: "Can do vii",
start: [
'if foo > 3:',
' bar|',
' if baz:',
' foo = 3',
'do_something_else()',
],
keysPressed: "viid",
end: [
'if foo > 3:',
'|do_something_else()',
],
endMode: ModeName.Normal
});

newTest({
title: "Doesn't naively select the next line",
start: [
'if foo > 3:',
' bar|',
'if foo > 3:',
' bar',
],
keysPressed: "viid",
end: [
'if foo > 3:',
'|if foo > 3:',
' bar',
],
endMode: ModeName.Normal
});

newTest({
title: "Searches backwards if cursor line is empty",
start: [
'if foo > 3:',
' log("foo is big")',
'|',
' foo = 3',
'do_something_else()',
],
keysPressed: "viid",
end: [
'if foo > 3:',
'|do_something_else()',
],
endMode: ModeName.Normal
});

newTest({
title: "Can do vaI",
start: [
'if foo > 3:',
' log("foo is big")|',
' foo = 3',
'do_something_else()',
],
keysPressed: "vaId",
end: [
'|',
],
endMode: ModeName.Normal
});
});

});

0 comments on commit c57d370

Please sign in to comment.