Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for indent objects #1550

Merged
merged 9 commits into from
Apr 27, 2017
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
});
});

});