From fe8eb5dcbc23abd65f18fec54e9206246876d720 Mon Sep 17 00:00:00 2001 From: Fred Bricon Date: Fri, 18 Jul 2025 19:08:16 +0200 Subject: [PATCH] feat: allow Cmd+L / Cmd+I on empty selection to select the entire file Signed-off-by: Fred Bricon --- .../e2e/tests/KeyboardShortcuts.test.ts | 71 +++++++++++++++++-- extensions/vscode/package.json | 5 +- extensions/vscode/src/util/addCode.ts | 56 ++++++++++----- 3 files changed, 106 insertions(+), 26 deletions(-) diff --git a/extensions/vscode/e2e/tests/KeyboardShortcuts.test.ts b/extensions/vscode/e2e/tests/KeyboardShortcuts.test.ts index 5dff86d5e5b..c557708ef9a 100644 --- a/extensions/vscode/e2e/tests/KeyboardShortcuts.test.ts +++ b/extensions/vscode/e2e/tests/KeyboardShortcuts.test.ts @@ -4,12 +4,12 @@ import { InputBox, Key, TextEditor, + until, VSBrowser, WebDriver, WebElement, WebView, Workbench, - until, } from "vscode-extension-tester"; import { GlobalActions } from "../actions/Global.actions"; @@ -47,11 +47,31 @@ describe("Keyboard Shortcuts", () => { afterEach(async function () { this.timeout(DEFAULT_TIMEOUT.XL * 1000); + await cleanupChat(); await view.switchBack(); await editor.clearText(); await new EditorView().closeAllEditors(); }); + async function cleanupChat(chatInput?: WebElement) { + try { + if (!chatInput) { + chatInput = await TestUtils.waitForSuccess(async () => { + return GUISelectors.getMessageInputFieldAtIndex(view, 0); + }); + } + if ( + chatInput && + (await chatInput.isDisplayed()) && + (await chatInput.isEnabled()) + ) { + await chatInput.clear(); + } + } catch (e) { + console.error(`Failed to clear chat: ${e}`); + } + } + it("Should correctly undo and redo using keyboard shortcuts when writing a chat message", async () => { await GUIActions.executeFocusContinueInputShortcut(driver); ({ view } = await GUIActions.switchToReactIframe()); @@ -105,10 +125,8 @@ describe("Keyboard Shortcuts", () => { ); }).timeout(DEFAULT_TIMEOUT.XL); - it("Should not create a code block when Cmd+L is pressed without text highlighted", async () => { - const text = "Hello, world!"; - - await editor.setText(text); + it("Should not create a code block when Cmd+L is pressed on an empty document", async () => { + expect((await editor.getText()).trim()).to.equal(""); await GUIActions.executeFocusContinueInputShortcut(driver); @@ -174,6 +192,9 @@ describe("Keyboard Shortcuts", () => { await driver.wait(until.elementIsNotVisible(textInput), DEFAULT_TIMEOUT.XS); expect(await textInput.isDisplayed()).to.equal(false); + + // Make sure the view is visible again, so it can be cleared in afterEach() + await GUIActions.executeFocusContinueInputShortcut(driver); }).timeout(DEFAULT_TIMEOUT.XL); it("Should create a code block when Cmd+L is pressed with text highlighted", async () => { @@ -197,4 +218,44 @@ describe("Keyboard Shortcuts", () => { await GUIActions.executeFocusContinueInputShortcut(driver); }).timeout(DEFAULT_TIMEOUT.XL); + + it("Should create a code block with the whole file when Cmd+L is pressed on an empty line", async () => { + const text = "Hello,\n\n\nworld!"; + + await editor.setText(text); + await editor.moveCursor(2, 1); //Move cursor to an empty line + + await GUIActions.executeFocusContinueInputShortcut(driver); + + ({ view } = await GUIActions.switchToReactIframe()); + + const codeBlock = await TestUtils.waitForSuccess(() => + GUISelectors.getInputBoxCodeBlockAtIndex(view, 0), + ); + const codeblockContent = await codeBlock.getAttribute( + "data-codeblockcontent", + ); + + expect(codeblockContent).to.equal(text); + }); + + it("Should create a code block when Cmd+L is pressed on a non-empty line", async () => { + const text = "Hello, world!"; + + await editor.setText(text); + await editor.moveCursor(1, 7); //Move cursor to the 1st space + + await GUIActions.executeFocusContinueInputShortcut(driver); + + ({ view } = await GUIActions.switchToReactIframe()); + + const codeBlock = await TestUtils.waitForSuccess(() => + GUISelectors.getInputBoxCodeBlockAtIndex(view, 0), + ); + const codeblockContent = await codeBlock.getAttribute( + "data-codeblockcontent", + ); + + expect(codeblockContent).to.equal(text); + }).timeout(DEFAULT_TIMEOUT.XL); }); diff --git a/extensions/vscode/package.json b/extensions/vscode/package.json index 3eaf44c2156..63b7f00d629 100644 --- a/extensions/vscode/package.json +++ b/extensions/vscode/package.json @@ -540,13 +540,12 @@ "continue.continueSubMenu": [ { "command": "continue.focusContinueInputWithoutClear", - "group": "Continue", - "when": "editorHasSelection" + "group": "Continue" }, { "command": "continue.focusEdit", "group": "Continue", - "when": "editorHasSelection && !editorReadonly" + "when": "!editorReadonly" } ], "explorer/context": [ diff --git a/extensions/vscode/src/util/addCode.ts b/extensions/vscode/src/util/addCode.ts index 524d77e8817..05d9c3afe24 100644 --- a/extensions/vscode/src/util/addCode.ts +++ b/extensions/vscode/src/util/addCode.ts @@ -36,22 +36,38 @@ export function getRangeInFileWithContents( }; } - if (selection.isEmpty && !allowEmpty) { + if ((selection.isEmpty && !allowEmpty) || isEmptyFile(editor.document)) { return null; } - let selectionRange = new vscode.Range(selection.start, selection.end); - const document = editor.document; - // Select the context from the beginning of the selection start line to the selection start position - const beginningOfSelectionStartLine = selection.start.with(undefined, 0); - const textBeforeSelectionStart = document.getText( - new vscode.Range(beginningOfSelectionStartLine, selection.start), - ); - // If there are only whitespace before the start of the selection, include the indentation - if (textBeforeSelectionStart.trim().length === 0) { - selectionRange = selectionRange.with({ - start: beginningOfSelectionStartLine, - }); + let selectionRange: vscode.Range | undefined; + // if the selection is empty and document is not empty, select the whole document + if (selection.isEmpty) { + selectionRange = new vscode.Range( + new vscode.Position(0, 0), + new vscode.Position( + editor.document.lineCount - 1, + editor.document.lineAt( + editor.document.lineCount - 1, + ).range.end.character, + ), + ); + } + + if (!selectionRange) { + selectionRange = new vscode.Range(selection.start, selection.end); + const document = editor.document; + // Select the context from the beginning of the selection start line to the selection start position + const beginningOfSelectionStartLine = selection.start.with(undefined, 0); + const textBeforeSelectionStart = document.getText( + new vscode.Range(beginningOfSelectionStartLine, selection.start), + ); + // If there are only whitespace before the start of the selection, include the indentation + if (textBeforeSelectionStart.trim().length === 0) { + selectionRange = selectionRange.with({ + start: beginningOfSelectionStartLine, + }); + } } const contents = editor.document.getText(selectionRange); @@ -61,12 +77,12 @@ export function getRangeInFileWithContents( contents, range: { start: { - line: selection.start.line, - character: selection.start.character, + line: selectionRange.start.line, + character: selectionRange.start.character, }, end: { - line: selection.end.line, - character: selection.end.character, + line: selectionRange.end.line, + character: selectionRange.end.character, }, }, }; @@ -78,7 +94,7 @@ export function getRangeInFileWithContents( export async function addHighlightedCodeToContext( webviewProtocol: VsCodeWebviewProtocol | undefined, ) { - const rangeInFileWithContents = getRangeInFileWithContents(); + const rangeInFileWithContents = getRangeInFileWithContents(true); if (rangeInFileWithContents) { webviewProtocol?.request("highlightedCode", { rangeInFileWithContents, @@ -129,6 +145,10 @@ export async function addEntireFileToContext( }); } +export function isEmptyFile(document: vscode.TextDocument) { + return document.lineCount === 1 && document.lineAt(0).range.isEmpty; +} + export function addCodeToContextFromRange( range: vscode.Range, webviewProtocol: VsCodeWebviewProtocol,