Skip to content

Commit

Permalink
Text edit based indent (#2518)
Browse files Browse the repository at this point in the history
## Checklist

- [/] I have added
[tests](https://www.cursorless.org/docs/contributing/test-case-recorder/)
- [/] I have updated the
[docs](https://github.com/cursorless-dev/cursorless/tree/main/docs) and
[cheatsheet](https://github.com/cursorless-dev/cursorless/tree/main/cursorless-talon/src/cheatsheet)
- [/] I have not broken the cheatsheet

---------

Co-authored-by: Pokey Rule <[email protected]>
  • Loading branch information
AndreasArvidsson and pokey authored Jul 30, 2024
1 parent 62ceed4 commit d323e35
Show file tree
Hide file tree
Showing 8 changed files with 221 additions and 8 deletions.
30 changes: 30 additions & 0 deletions data/fixtures/recorded/actions/dedentLine.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
languageId: plaintext
command:
version: 7
spokenForm: dedent line
action:
name: outdentLine
target:
type: primitive
modifiers:
- type: containingScope
scopeType: {type: line}
usePrePhraseSnapshot: true
initialState:
documentContents: " foo"
selections:
- anchor: {line: 0, character: 0}
active: {line: 0, character: 0}
marks: {}
finalState:
documentContents: foo
selections:
- anchor: {line: 0, character: 0}
active: {line: 0, character: 0}
thatMark:
- type: UntypedTarget
contentRange:
start: {line: 0, character: 0}
end: {line: 0, character: 3}
isReversed: false
hasExplicitRange: true
30 changes: 30 additions & 0 deletions data/fixtures/recorded/actions/indentLine.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
languageId: plaintext
command:
version: 7
spokenForm: indent line
action:
name: indentLine
target:
type: primitive
modifiers:
- type: containingScope
scopeType: {type: line}
usePrePhraseSnapshot: true
initialState:
documentContents: foo
selections:
- anchor: {line: 0, character: 0}
active: {line: 0, character: 0}
marks: {}
finalState:
documentContents: " foo"
selections:
- anchor: {line: 0, character: 4}
active: {line: 0, character: 4}
thatMark:
- type: UntypedTarget
contentRange:
start: {line: 0, character: 4}
end: {line: 0, character: 7}
isReversed: false
hasExplicitRange: true
3 changes: 1 addition & 2 deletions packages/cursorless-engine/src/actions/Actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,11 @@ import {
} from "./SetSelection";
import { SetSpecialTarget } from "./SetSpecialTarget";
import ShowParseTree from "./ShowParseTree";
import { IndentLine, OutdentLine } from "./IndentLine";
import {
CopyToClipboard,
ExtractVariable,
Fold,
IndentLine,
OutdentLine,
Rename,
RevealDefinition,
RevealTypeDefinition,
Expand Down
151 changes: 151 additions & 0 deletions packages/cursorless-engine/src/actions/IndentLine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { FlashStyle, Range, type TextEditor } from "@cursorless/common";
import { flatten, zip } from "lodash-es";
import { selectionToStoredTarget } from "../core/commandRunner/selectionToStoredTarget";
import type { RangeUpdater } from "../core/updateSelections/RangeUpdater";
import { ide } from "../singletons/ide.singleton";
import { Target } from "../typings/target.types";
import { flashTargets, runOnTargetsForEachEditor } from "../util/targetUtils";
import {
IndentLineSimpleAction,
OutdentLineSimpleAction,
} from "./SimpleIdeCommandActions";
import { ActionReturnValue } from "./actions.types";
import { performEditsAndUpdateSelections } from "../core/updateSelections/updateSelections";

abstract class IndentLineBase {
constructor(
private rangeUpdater: RangeUpdater,
private isIndent: boolean,
) {
this.run = this.run.bind(this);
this.runForEditor = this.runForEditor.bind(this);
}

async run(targets: Target[]): Promise<ActionReturnValue> {
if (this.hasCapability()) {
return this.runSimpleCommandAction(targets);
}

await flashTargets(ide(), targets, FlashStyle.pendingModification0);

const thatTargets = flatten(
await runOnTargetsForEachEditor(targets, this.runForEditor),
);

return { thatTargets };
}

private hasCapability() {
return this.isIndent
? ide().capabilities.commands.indentLine != null
: ide().capabilities.commands.outdentLine != null;
}

private runSimpleCommandAction(
targets: Target[],
): Promise<ActionReturnValue> {
const action = this.isIndent
? new IndentLineSimpleAction(this.rangeUpdater)
: new OutdentLineSimpleAction(this.rangeUpdater);
return action.run(targets);
}

private async runForEditor(editor: TextEditor, targets: Target[]) {
const edits = this.isIndent
? getIndentEdits(editor, targets)
: getOutdentEdits(editor, targets);

const { targetSelections: updatedTargetSelections } =
await performEditsAndUpdateSelections({
rangeUpdater: this.rangeUpdater,
editor: ide().getEditableTextEditor(editor),
edits,
selections: {
targetSelections: targets.map(
({ contentSelection }) => contentSelection,
),
},
});

return zip(targets, updatedTargetSelections).map(([target, range]) =>
selectionToStoredTarget({
editor,
selection: range!.toSelection(target!.isReversed),
}),
);
}
}

export class IndentLine extends IndentLineBase {
constructor(rangeUpdater: RangeUpdater) {
super(rangeUpdater, true);
}
}

export class OutdentLine extends IndentLineBase {
constructor(rangeUpdater: RangeUpdater) {
super(rangeUpdater, false);
}
}

function getIndentEdits(editor: TextEditor, targets: Target[]) {
const { document } = editor;
const lineNumbers = getLineNumbers(targets);
const indent = getIndent(editor);
return lineNumbers.map((lineNumber) => {
const line = document.lineAt(lineNumber);
return {
range: line.range.start.toEmptyRange(),
text: indent,
};
});
}

function getOutdentEdits(editor: TextEditor, targets: Target[]) {
const { document } = editor;
const lineNumbers = getLineNumbers(targets);
const regex = getRegex(editor);
return lineNumbers.map((lineNumber) => {
const line = document.lineAt(lineNumber);
const match = line.text.match(regex);
const { start } = line.range;
const end = start.translate(undefined, match?.[0].length);
return {
range: new Range(start, end),
text: "",
};
});
}

function getLineNumbers(targets: Target[]) {
const lineNumbers = new Set<number>();
for (const target of targets) {
const { start, end } = target.contentRange;
for (let i = start.line; i <= end.line; ++i) {
lineNumbers.add(i);
}
}
return [...lineNumbers];
}

function getIndent(editor: TextEditor) {
if (editor.options.insertSpaces) {
const tabSize = getTabSize(editor);
return " ".repeat(tabSize);
}
return "\t";
}

function getRegex(editor: TextEditor) {
if (editor.options.insertSpaces) {
const tabSize = getTabSize(editor);
return new RegExp(`^[ ]{1,${tabSize}}`);
}
return /^\t/;
}

function getTabSize(editor: TextEditor): number {
return typeof editor.options.tabSize === "number"
? editor.options.tabSize
: 4;
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,11 @@ export class ToggleLineComment extends SimpleIdeCommandAction {
command: CommandId = "toggleLineComment";
}

export class IndentLine extends SimpleIdeCommandAction {
export class IndentLineSimpleAction extends SimpleIdeCommandAction {
command: CommandId = "indentLine";
}

export class OutdentLine extends SimpleIdeCommandAction {
export class OutdentLineSimpleAction extends SimpleIdeCommandAction {
command: CommandId = "outdentLine";
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import type { Capabilities, CommandCapabilityMap } from "@cursorless/common";

const COMMAND_CAPABILITIES: CommandCapabilityMap = {
indentLine: { acceptsLocation: true },
outdentLine: { acceptsLocation: true },
clipboardCopy: { acceptsLocation: true },

toggleLineComment: undefined,
Expand All @@ -18,6 +16,8 @@ const COMMAND_CAPABILITIES: CommandCapabilityMap = {
unfold: undefined,
showReferences: undefined,
insertLineAfter: undefined,
indentLine: undefined,
outdentLine: undefined,
};

export class TalonJsCapabilities implements Capabilities {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,11 @@ export class TalonJsEditor implements EditableTextEditor {
}

indentLine(_ranges: Range[]): Promise<void> {
throw Error(`indentLine not implemented.`);
throw Error("indentLine not implemented.");
}

outdentLine(_ranges: Range[]): Promise<void> {
throw Error(`outdentLine not implemented.`);
throw Error("outdentLine not implemented.");
}

insertLineAfter(_ranges?: Range[]): Promise<void> {
Expand Down
3 changes: 3 additions & 0 deletions packages/cursorless-neovim-e2e/src/shouldRunTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ function isFailingFixture(name: string, fixture: TestCaseFixtureLegacy) {
// "recorded/actions/copySecondToken" -> wrong fixture.finalState.clipboard
case "copyToClipboard":
return true;
case "indentLine":
case "outdentLine":
return true;
}

// "recorded/lineEndings/*" -> fixture.finalState.documentContents contains \n instead of \r\n
Expand Down

0 comments on commit d323e35

Please sign in to comment.