From 2f75a99f597fbb401ba4fa9b24b71c099b2623be Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 11 Mar 2026 12:02:21 +0100 Subject: [PATCH 1/4] Support staging and unstaging entire files with git actions --- .../src/ide/vscode/VscodeTextEditorImpl.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/cursorless-vscode/src/ide/vscode/VscodeTextEditorImpl.ts b/packages/cursorless-vscode/src/ide/vscode/VscodeTextEditorImpl.ts index e86065b41c..04add4c2c0 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VscodeTextEditorImpl.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VscodeTextEditorImpl.ts @@ -252,10 +252,25 @@ export class VscodeTextEditorImpl implements EditableTextEditor { } public async gitStage(_range?: Range): Promise { - await vscode.commands.executeCommand("git.stageSelectedRanges"); + if (this.selectionIsEntireFile()) { + await vscode.commands.executeCommand("git.stage"); + } else { + await vscode.commands.executeCommand("git.stageSelectedRanges"); + } } public async gitUnstage(_range?: Range): Promise { - await vscode.commands.executeCommand("git.unstageSelectedRanges"); + if (this.selectionIsEntireFile()) { + await vscode.commands.executeCommand("git.unstage"); + } else { + await vscode.commands.executeCommand("git.unstageSelectedRanges"); + } + } + + private selectionIsEntireFile(): boolean { + return ( + this.selections.length === 1 && + this.selections[0].isRangeEqual(this.document.range) + ); } } From 44df877c8c1c34115ace838039020ad66aaab315 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 11 Mar 2026 12:03:57 +0100 Subject: [PATCH 2/4] Update function name --- .../src/ide/vscode/VscodeTextEditorImpl.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/cursorless-vscode/src/ide/vscode/VscodeTextEditorImpl.ts b/packages/cursorless-vscode/src/ide/vscode/VscodeTextEditorImpl.ts index 04add4c2c0..a873f3332f 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VscodeTextEditorImpl.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VscodeTextEditorImpl.ts @@ -252,7 +252,7 @@ export class VscodeTextEditorImpl implements EditableTextEditor { } public async gitStage(_range?: Range): Promise { - if (this.selectionIsEntireFile()) { + if (this.selectionIsEntireDocument()) { await vscode.commands.executeCommand("git.stage"); } else { await vscode.commands.executeCommand("git.stageSelectedRanges"); @@ -260,14 +260,14 @@ export class VscodeTextEditorImpl implements EditableTextEditor { } public async gitUnstage(_range?: Range): Promise { - if (this.selectionIsEntireFile()) { + if (this.selectionIsEntireDocument()) { await vscode.commands.executeCommand("git.unstage"); } else { await vscode.commands.executeCommand("git.unstageSelectedRanges"); } } - private selectionIsEntireFile(): boolean { + private selectionIsEntireDocument(): boolean { return ( this.selections.length === 1 && this.selections[0].isRangeEqual(this.document.range) From 2b08c14dc656260904f9f5c4aef4c4d338827fc2 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 11 Mar 2026 12:54:12 +0100 Subject: [PATCH 3/4] Added stage and unstage file as separate functions in the editor interface --- packages/common/src/types/TextEditor.ts | 14 ++++++++-- .../src/actions/SimpleIdeCommandActions.ts | 25 ++++++++++++++--- .../src/ide/TalonJsEditor.ts | 16 ++++++++--- .../src/ide/vscode/VscodeTextEditorImpl.ts | 27 +++++++------------ .../src/ide/neovim/NeovimTextEditorImpl.ts | 16 ++++++++--- 5 files changed, 67 insertions(+), 31 deletions(-) diff --git a/packages/common/src/types/TextEditor.ts b/packages/common/src/types/TextEditor.ts index 894fbce780..6670a63c1b 100644 --- a/packages/common/src/types/TextEditor.ts +++ b/packages/common/src/types/TextEditor.ts @@ -250,15 +250,25 @@ export interface EditableTextEditor extends TextEditor { */ gitRevert(range?: Range): Promise; + /** + * Git stage file + */ + gitStageFile(): Promise; + + /** + * Git unstage file + */ + gitUnstageFile(): Promise; + /** * Git stage range * @param range A {@link Range range} */ - gitStage(range?: Range): Promise; + gitStageRange(range?: Range): Promise; /** * Git unstage range * @param range A {@link Range range} */ - gitUnstage(range?: Range): Promise; + gitUnstageRange(range?: Range): Promise; } diff --git a/packages/cursorless-engine/src/actions/SimpleIdeCommandActions.ts b/packages/cursorless-engine/src/actions/SimpleIdeCommandActions.ts index 9b9093fcc9..e10906260b 100644 --- a/packages/cursorless-engine/src/actions/SimpleIdeCommandActions.ts +++ b/packages/cursorless-engine/src/actions/SimpleIdeCommandActions.ts @@ -46,7 +46,8 @@ abstract class SimpleIdeCommandAction { callback: (editor, targets) => callback( editor, - acceptsLocation ? targets.map((t) => t.contentRange) : undefined, + targets.map((t) => t.contentRange), + acceptsLocation, this.command, ), setSelection: !acceptsLocation, @@ -150,9 +151,12 @@ export class GitUnstage extends SimpleIdeCommandAction { function callback( editor: EditableTextEditor, - ranges: Range[] | undefined, + targetRanges: Range[], + acceptsLocation: boolean, command: CommandId, ): Promise { + const ranges = acceptsLocation ? targetRanges : undefined; + switch (command) { // Multi target actions case "toggleLineComment": @@ -192,12 +196,25 @@ function callback( case "gitRevert": return editor.gitRevert(ranges?.[0]); case "gitStage": - return editor.gitStage(ranges?.[0]); + if (rangeIsEntireDocument(editor, targetRanges)) { + return editor.gitStageFile(); + } + return editor.gitStageRange(ranges?.[0]); case "gitUnstage": - return editor.gitUnstage(ranges?.[0]); + if (rangeIsEntireDocument(editor, targetRanges)) { + return editor.gitUnstageFile(); + } + return editor.gitUnstageRange(ranges?.[0]); // Unsupported as simple action case "highlight": throw Error("Highlight command not supported as simple action"); } } + +function rangeIsEntireDocument( + editor: EditableTextEditor, + ranges: Range[], +): boolean { + return ranges.length === 1 && ranges[0].isRangeEqual(editor.document.range); +} diff --git a/packages/cursorless-everywhere-talon-core/src/ide/TalonJsEditor.ts b/packages/cursorless-everywhere-talon-core/src/ide/TalonJsEditor.ts index da4264bac2..c6592c0c65 100644 --- a/packages/cursorless-everywhere-talon-core/src/ide/TalonJsEditor.ts +++ b/packages/cursorless-everywhere-talon-core/src/ide/TalonJsEditor.ts @@ -163,11 +163,19 @@ export class TalonJsEditor implements EditableTextEditor { throw Error("gitRevert not implemented"); } - public async gitStage(_range?: Range): Promise { - throw Error("gitStage not implemented"); + public async gitStageFile(): Promise { + throw Error("gitStageFile not implemented"); } - public async gitUnstage(_range?: Range): Promise { - throw Error("gitUnstage not implemented"); + public async gitUnstageFile(): Promise { + throw Error("gitUnstageFile not implemented"); + } + + public async gitStageRange(_range?: Range): Promise { + throw Error("gitStageRange not implemented"); + } + + public async gitUnstageRange(_range?: Range): Promise { + throw Error("gitUnstageRange not implemented"); } } diff --git a/packages/cursorless-vscode/src/ide/vscode/VscodeTextEditorImpl.ts b/packages/cursorless-vscode/src/ide/vscode/VscodeTextEditorImpl.ts index a873f3332f..81128c183e 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VscodeTextEditorImpl.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VscodeTextEditorImpl.ts @@ -251,26 +251,19 @@ export class VscodeTextEditorImpl implements EditableTextEditor { await vscode.commands.executeCommand("git.revertSelectedRanges"); } - public async gitStage(_range?: Range): Promise { - if (this.selectionIsEntireDocument()) { - await vscode.commands.executeCommand("git.stage"); - } else { - await vscode.commands.executeCommand("git.stageSelectedRanges"); - } + public async gitStageFile(): Promise { + await vscode.commands.executeCommand("git.stage"); } - public async gitUnstage(_range?: Range): Promise { - if (this.selectionIsEntireDocument()) { - await vscode.commands.executeCommand("git.unstage"); - } else { - await vscode.commands.executeCommand("git.unstageSelectedRanges"); - } + public async gitUnstageFile(): Promise { + await vscode.commands.executeCommand("git.unstage"); + } + + public async gitStageRange(_range?: Range): Promise { + await vscode.commands.executeCommand("git.stageSelectedRanges"); } - private selectionIsEntireDocument(): boolean { - return ( - this.selections.length === 1 && - this.selections[0].isRangeEqual(this.document.range) - ); + public async gitUnstageRange(_range?: Range): Promise { + await vscode.commands.executeCommand("git.unstageSelectedRanges"); } } diff --git a/packages/neovim-common/src/ide/neovim/NeovimTextEditorImpl.ts b/packages/neovim-common/src/ide/neovim/NeovimTextEditorImpl.ts index 8671e17cde..3be47d4c77 100644 --- a/packages/neovim-common/src/ide/neovim/NeovimTextEditorImpl.ts +++ b/packages/neovim-common/src/ide/neovim/NeovimTextEditorImpl.ts @@ -199,11 +199,19 @@ export class NeovimTextEditorImpl implements EditableTextEditor { throw Error("gitRevert Not implemented"); } - public async gitStage(_range?: Range): Promise { - throw Error("gitStage Not implemented"); + public async gitStageFile(): Promise { + throw Error("gitStageFile not implemented"); } - public async gitUnstage(_range?: Range): Promise { - throw Error("gitUnstage Not implemented"); + public async gitUnstageFile(): Promise { + throw Error("gitUnstageFile not implemented"); + } + + public async gitStageRange(_range?: Range): Promise { + throw Error("gitStageRange not implemented"); + } + + public async gitUnstageRange(_range?: Range): Promise { + throw Error("gitUnstageRange not implemented"); } } From 9c5d1bc12c0d9d213c306113c23eaa0a6b328c2b Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 11 Mar 2026 12:59:29 +0100 Subject: [PATCH 4/4] simplification --- .../src/actions/SimpleIdeCommandActions.ts | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/packages/cursorless-engine/src/actions/SimpleIdeCommandActions.ts b/packages/cursorless-engine/src/actions/SimpleIdeCommandActions.ts index e10906260b..a6d9668413 100644 --- a/packages/cursorless-engine/src/actions/SimpleIdeCommandActions.ts +++ b/packages/cursorless-engine/src/actions/SimpleIdeCommandActions.ts @@ -46,8 +46,8 @@ abstract class SimpleIdeCommandAction { callback: (editor, targets) => callback( editor, - targets.map((t) => t.contentRange), - acceptsLocation, + acceptsLocation ? targets.map((t) => t.contentRange) : undefined, + rangeIsEntireDocument(targets), this.command, ), setSelection: !acceptsLocation, @@ -151,12 +151,10 @@ export class GitUnstage extends SimpleIdeCommandAction { function callback( editor: EditableTextEditor, - targetRanges: Range[], - acceptsLocation: boolean, + ranges: Range[] | undefined, + rangeIsEntireDocument: boolean, command: CommandId, ): Promise { - const ranges = acceptsLocation ? targetRanges : undefined; - switch (command) { // Multi target actions case "toggleLineComment": @@ -196,12 +194,12 @@ function callback( case "gitRevert": return editor.gitRevert(ranges?.[0]); case "gitStage": - if (rangeIsEntireDocument(editor, targetRanges)) { + if (rangeIsEntireDocument) { return editor.gitStageFile(); } return editor.gitStageRange(ranges?.[0]); case "gitUnstage": - if (rangeIsEntireDocument(editor, targetRanges)) { + if (rangeIsEntireDocument) { return editor.gitUnstageFile(); } return editor.gitUnstageRange(ranges?.[0]); @@ -212,9 +210,9 @@ function callback( } } -function rangeIsEntireDocument( - editor: EditableTextEditor, - ranges: Range[], -): boolean { - return ranges.length === 1 && ranges[0].isRangeEqual(editor.document.range); +function rangeIsEntireDocument(targets: Target[]): boolean { + return ( + targets.length === 1 && + targets[0].contentRange.isRangeEqual(targets[0].editor.document.range) + ); }