diff --git a/build/npm/preinstall.js b/build/npm/preinstall.js index 31821ee2393d1..41a17b016767f 100644 --- a/build/npm/preinstall.js +++ b/build/npm/preinstall.js @@ -23,6 +23,7 @@ if (process.env['npm_execpath'].includes('yarn')) { const path = require('path'); const fs = require('fs'); const cp = require('child_process'); +const os = require('os'); if (process.platform === 'win32') { if (!hasSupportedVisualStudioVersion()) { @@ -32,6 +33,11 @@ if (process.platform === 'win32') { installHeaders(); } +if (process.arch !== os.arch()) { + console.error(`\x1b[1;31m*** ARCHITECTURE MISMATCH: The node.js process is ${process.arch}, but your OS architecture is ${os.arch()}. ***\x1b[0;0m`); + console.error(`\x1b[1;31m*** This can greatly increase the build time of vs code. ***\x1b[0;0m`); +} + function hasSupportedVisualStudioVersion() { const fs = require('fs'); const path = require('path'); diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index b348e7a73fc7e..82c00c2196a13 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -17,9 +17,10 @@ import { getBashGlobals } from './shell/bash'; import { getZshGlobals } from './shell/zsh'; import { getFishGlobals } from './shell/fish'; import { getPwshGlobals } from './shell/pwsh'; +import { getTokenType, TokenType } from './tokens'; // TODO: remove once API is finalized -export enum TerminalShellType { +export const enum TerminalShellType { Sh = 1, Bash = 2, Fish = 3, @@ -103,7 +104,8 @@ export async function activate(context: vscode.ExtensionContext) { const prefix = getPrefix(terminalContext.commandLine, terminalContext.cursorPosition); const pathSeparator = isWindows ? '\\' : '/'; - const result = await getCompletionItemsFromSpecs(availableSpecs, terminalContext, commands, prefix, terminal.shellIntegration?.cwd, token); + const tokenType = getTokenType(terminalContext, shellType); + const result = await getCompletionItemsFromSpecs(availableSpecs, terminalContext, commands, prefix, tokenType, terminal.shellIntegration?.cwd, token); if (terminal.shellIntegration?.env) { const homeDirCompletion = result.items.find(i => i.label === '~'); if (homeDirCompletion && terminal.shellIntegration.env.HOME) { @@ -288,6 +290,7 @@ export async function getCompletionItemsFromSpecs( terminalContext: { commandLine: string; cursorPosition: number }, availableCommands: ICompletionResource[], prefix: string, + tokenType: TokenType, shellIntegrationCwd?: vscode.Uri, token?: vscode.CancellationToken ): Promise<{ items: vscode.TerminalCompletionItem[]; filesRequested: boolean; foldersRequested: boolean; cwd?: vscode.Uri }> { @@ -295,7 +298,6 @@ export async function getCompletionItemsFromSpecs( let filesRequested = false; let foldersRequested = false; - const firstCommand = getFirstCommand(terminalContext.commandLine); const precedingText = terminalContext.commandLine.slice(0, terminalContext.cursorPosition + 1); for (const spec of specs) { @@ -311,17 +313,13 @@ export async function getCompletionItemsFromSpecs( continue; } - if ( - // If the prompt is empty - !terminalContext.commandLine - // or the first command matches the command - || !!firstCommand && specLabel.startsWith(firstCommand) - ) { - // push it to the completion items + // push it to the completion items + if (tokenType === TokenType.Command) { items.push(createCompletionItem(terminalContext.cursorPosition, prefix, { label: specLabel }, getDescription(spec), availableCommand.detail)); + continue; } - if (!terminalContext.commandLine.startsWith(specLabel)) { + if (!terminalContext.commandLine.startsWith(`${specLabel} `)) { // the spec label is not the first word in the command line, so do not provide options or args continue; } @@ -349,9 +347,7 @@ export async function getCompletionItemsFromSpecs( !filesRequested && !foldersRequested; - const shouldShowCommands = !terminalContext.commandLine.substring(0, terminalContext.cursorPosition).trimStart().includes(' '); - - if (shouldShowCommands && !filesRequested && !foldersRequested) { + if (tokenType === TokenType.Command) { // Include builitin/available commands in the results const labels = new Set(items.map((i) => i.label)); for (const command of availableCommands) { @@ -500,19 +496,6 @@ function getCompletionItemsFromArgs(args: Fig.SingleOrArray | undefined return { items, filesRequested, foldersRequested }; } - - -function getFirstCommand(commandLine: string): string | undefined { - const wordsOnLine = commandLine.split(' '); - let firstCommand: string | undefined = wordsOnLine[0]; - if (wordsOnLine.length > 1) { - firstCommand = undefined; - } else if (wordsOnLine.length === 0) { - firstCommand = commandLine; - } - return firstCommand; -} - function getFriendlyResourcePath(uri: vscode.Uri, pathSeparator: string, kind?: vscode.TerminalCompletionItemKind): string { let path = uri.fsPath; // Ensure drive is capitalized on Windows diff --git a/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts b/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts index d72996b8f702b..030e9d0db9953 100644 --- a/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts +++ b/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts @@ -12,6 +12,7 @@ import codeCompletionSpec from '../completions/code'; import codeInsidersCompletionSpec from '../completions/code-insiders'; import type { Uri } from 'vscode'; import { basename } from 'path'; +import { getTokenType } from '../tokens'; const fixtureDir = vscode.Uri.joinPath(vscode.Uri.file(__dirname), '../../testWorkspace'); const testCwdParent = vscode.Uri.joinPath(fixtureDir, 'parent'); @@ -150,7 +151,8 @@ suite('Terminal Suggest', () => { const prefix = commandLine.slice(0, cursorPosition).split(' ').at(-1) || ''; const filesRequested = testSpec.expectedResourceRequests?.type === 'files' || testSpec.expectedResourceRequests?.type === 'both'; const foldersRequested = testSpec.expectedResourceRequests?.type === 'folders' || testSpec.expectedResourceRequests?.type === 'both'; - const result = await getCompletionItemsFromSpecs(completionSpecs, { commandLine, cursorPosition }, availableCommands.map(c => { return { label: c }; }), prefix, testCwd); + const terminalContext = { commandLine, cursorPosition }; + const result = await getCompletionItemsFromSpecs(completionSpecs, terminalContext, availableCommands.map(c => { return { label: c }; }), prefix, getTokenType(terminalContext, undefined), testCwd); deepStrictEqual(result.items.map(i => i.label).sort(), (testSpec.expectedCompletions ?? []).sort()); strictEqual(result.filesRequested, filesRequested); strictEqual(result.foldersRequested, foldersRequested); diff --git a/extensions/terminal-suggest/src/test/tokens.test.ts b/extensions/terminal-suggest/src/test/tokens.test.ts new file mode 100644 index 0000000000000..621a9b653883e --- /dev/null +++ b/extensions/terminal-suggest/src/test/tokens.test.ts @@ -0,0 +1,52 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'mocha'; +import { strictEqual } from 'node:assert'; +import { getTokenType, TokenType } from '../tokens'; +import { TerminalShellType } from '../terminalSuggestMain'; + +suite('Terminal Suggest', () => { + test('simple command', () => { + strictEqual(getTokenType({ commandLine: 'echo', cursorPosition: 'echo'.length }, undefined), TokenType.Command); + }); + test('simple argument', () => { + strictEqual(getTokenType({ commandLine: 'echo hello', cursorPosition: 'echo hello'.length }, undefined), TokenType.Argument); + }); + test('simple command, cursor mid text', () => { + strictEqual(getTokenType({ commandLine: 'echo hello', cursorPosition: 'echo'.length }, undefined), TokenType.Command); + }); + test('simple argument, cursor mid text', () => { + strictEqual(getTokenType({ commandLine: 'echo hello', cursorPosition: 'echo hel'.length }, undefined), TokenType.Argument); + }); + suite('reset to command', () => { + test('|', () => { + strictEqual(getTokenType({ commandLine: 'echo hello | ', cursorPosition: 'echo hello | '.length }, undefined), TokenType.Command); + }); + test(';', () => { + strictEqual(getTokenType({ commandLine: 'echo hello; ', cursorPosition: 'echo hello; '.length }, undefined), TokenType.Command); + }); + test('&&', () => { + strictEqual(getTokenType({ commandLine: 'echo hello && ', cursorPosition: 'echo hello && '.length }, undefined), TokenType.Command); + }); + test('||', () => { + strictEqual(getTokenType({ commandLine: 'echo hello || ', cursorPosition: 'echo hello || '.length }, undefined), TokenType.Command); + }); + }); + suite('pwsh', () => { + test('simple command', () => { + strictEqual(getTokenType({ commandLine: 'Write-Host', cursorPosition: 'Write-Host'.length }, TerminalShellType.PowerShell), TokenType.Command); + }); + test('simple argument', () => { + strictEqual(getTokenType({ commandLine: 'Write-Host hello', cursorPosition: 'Write-Host hello'.length }, TerminalShellType.PowerShell), TokenType.Argument); + }); + test('reset char', () => { + strictEqual(getTokenType({ commandLine: `Write-Host hello -and `, cursorPosition: `Write-Host hello -and `.length }, TerminalShellType.PowerShell), TokenType.Command); + }); + test('arguments after reset char', () => { + strictEqual(getTokenType({ commandLine: `Write-Host hello -and $true `, cursorPosition: `Write-Host hello -and $true `.length }, TerminalShellType.PowerShell), TokenType.Argument); + }); + }); +}); diff --git a/extensions/terminal-suggest/src/tokens.ts b/extensions/terminal-suggest/src/tokens.ts new file mode 100644 index 0000000000000..9712d2b0bfee5 --- /dev/null +++ b/extensions/terminal-suggest/src/tokens.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { TerminalShellType } from './terminalSuggestMain'; + +export const enum TokenType { + Command, + Argument, +} + +const shellTypeResetChars: { [key: number]: string[] | undefined } = { + [TerminalShellType.Bash]: ['>', '>>', '<', '2>', '2>>', '&>', '&>>', '|', '|&', '&&', '||', '&', ';', '(', '{', '<<'], + [TerminalShellType.Zsh]: ['>', '>>', '<', '2>', '2>>', '&>', '&>>', '<>', '|', '|&', '&&', '||', '&', ';', '(', '{', '<<', '<<<', '<('], + [TerminalShellType.PowerShell]: ['>', '>>', '<', '2>', '2>>', '*>', '*>>', '|', '-and', '-or', '-not', '!', '&', '-eq', '-ne', '-gt', '-lt', '-ge', '-le', '-like', '-notlike', '-match', '-notmatch', '-contains', '-notcontains', '-in', '-notin'] +}; + +const defaultShellTypeResetChars = shellTypeResetChars[TerminalShellType.Bash]!; + +export function getTokenType(ctx: { commandLine: string; cursorPosition: number }, shellType: TerminalShellType | undefined): TokenType { + const spaceIndex = ctx.commandLine.substring(0, ctx.cursorPosition).lastIndexOf(' '); + if (spaceIndex === -1) { + return TokenType.Command; + } + const tokenIndex = spaceIndex === -1 ? 0 : spaceIndex + 1; + const previousTokens = ctx.commandLine.substring(0, tokenIndex).trim(); + const commandResetChars = shellType === undefined ? defaultShellTypeResetChars : shellTypeResetChars[shellType] ?? defaultShellTypeResetChars; + if (commandResetChars.some(e => previousTokens.endsWith(e))) { + return TokenType.Command; + } + return TokenType.Argument; +} diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/editor.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/editor.test.ts index 070a169968447..106ef702d6934 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/editor.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/editor.test.ts @@ -94,6 +94,37 @@ suite('vscode API - editors', () => { }); }); + /** + * Given : + * This is line 1 + * | + * + * Expect : + * This is line 1 + * This is line 2 + * This is line 3 + * + * The 3rd line should not be auto-indented, as the edit already + * contains the necessary adjustment. + */ + test('insert snippet with replacement, avoid adjusting indentation', () => { + const snippetString = new SnippetString() + .appendText('This is line 2\n This is line 3'); + + return withRandomFileEditor('This is line 1\n ', (editor, doc) => { + editor.selection = new Selection( + new Position(1, 3), + new Position(1, 3) + ); + + return editor.insertSnippet(snippetString, undefined, { undoStopAfter: false, undoStopBefore: false, keepWhitespace: true }).then(inserted => { + assert.ok(inserted); + assert.strictEqual(doc.getText(), 'This is line 1\n This is line 2\n This is line 3'); + assert.ok(doc.isDirty); + }); + }); + }); + test('insert snippet with replacement, selection as argument', () => { const snippetString = new SnippetString() .appendText('has been'); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts index 90baf6d28702a..3fac3601f2b1a 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts @@ -1227,6 +1227,27 @@ suite('vscode API - workspace', () => { assert.deepStrictEqual(edt.selections, [new vscode.Selection(0, 0, 0, 3)]); }); + test('SnippetString in WorkspaceEdit with keepWhitespace', async function (): Promise { + const file = await createRandomFile('This is line 1\n '); + + const document = await vscode.workspace.openTextDocument(file); + const edt = await vscode.window.showTextDocument(document); + + assert.ok(edt === vscode.window.activeTextEditor); + + const snippetText = new vscode.SnippetTextEdit(new vscode.Range(1, 3, 1, 3), new vscode.SnippetString('This is line 2\n This is line 3')); + snippetText.keepWhitespace = true; + const we = new vscode.WorkspaceEdit(); + we.set(document.uri, [snippetText]); + const success = await vscode.workspace.applyEdit(we); + if (edt !== vscode.window.activeTextEditor) { + return this.skip(); + } + + assert.ok(success); + assert.strictEqual(document.getText(), 'This is line 1\n This is line 2\n This is line 3'); + }); + test('Support creating binary files in a WorkspaceEdit', async function (): Promise { const fileUri = vscode.Uri.parse(`${testFs.scheme}:/${rndName()}`); diff --git a/src/vs/editor/browser/services/bulkEditService.ts b/src/vs/editor/browser/services/bulkEditService.ts index cbc88fafc2f09..fa12a2ab99e5c 100644 --- a/src/vs/editor/browser/services/bulkEditService.ts +++ b/src/vs/editor/browser/services/bulkEditService.ts @@ -55,7 +55,7 @@ export class ResourceTextEdit extends ResourceEdit implements IWorkspaceTextEdit constructor( readonly resource: URI, - readonly textEdit: TextEdit & { insertAsSnippet?: boolean }, + readonly textEdit: TextEdit & { insertAsSnippet?: boolean; keepWhitespace?: boolean }, readonly versionId: number | undefined = undefined, metadata?: WorkspaceEditMetadata, ) { diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index 95aa2bfee68a3..76daa762217ea 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -1814,7 +1814,7 @@ export interface IWorkspaceFileEdit { export interface IWorkspaceTextEdit { resource: URI; - textEdit: TextEdit & { insertAsSnippet?: boolean }; + textEdit: TextEdit & { insertAsSnippet?: boolean; keepWhitespace?: boolean }; versionId: number | undefined; metadata?: WorkspaceEditMetadata; } diff --git a/src/vs/editor/common/standaloneStrings.ts b/src/vs/editor/common/standaloneStrings.ts index f2edfd33f60a6..c181573190baf 100644 --- a/src/vs/editor/common/standaloneStrings.ts +++ b/src/vs/editor/common/standaloneStrings.ts @@ -36,7 +36,7 @@ export namespace AccessibilityHelpNLS { export const debugExecuteSelection = nls.localize('debugConsole.executeSelection', "The Debug: Execute Selection command{0} will execute the selected text in the debug console.", ''); export const chatEditorModification = nls.localize('chatEditorModification', "The editor contains pending modifications that have been made by chat."); export const chatEditorRequestInProgress = nls.localize('chatEditorRequestInProgress', "The editor is currently waiting for modifications to be made by chat."); - export const chatEditActions = nls.localize('chatEditing.navigation', 'Navigate between edits in the editor with navigate previous{0} and next{1} and accept{3} and reject the current change{4}.', '', '', '', ''); + export const chatEditActions = nls.localize('chatEditing.navigation', 'Navigate between edits in the editor with navigate previous{0} and next{1} and accept{2}, reject{3} or view the diff{4} for the current change.', '', '', '', '', ''); } export namespace InspectTokensNLS { diff --git a/src/vs/editor/contrib/snippet/browser/snippetSession.ts b/src/vs/editor/contrib/snippet/browser/snippetSession.ts index 79cf57b3abf39..30e42dd539a6b 100644 --- a/src/vs/editor/contrib/snippet/browser/snippetSession.ts +++ b/src/vs/editor/contrib/snippet/browser/snippetSession.ts @@ -375,6 +375,7 @@ const _defaultOptions: ISnippetSessionInsertOptions = { export interface ISnippetEdit { range: Range; template: string; + keepWhitespace?: boolean; } export class SnippetSession { @@ -569,7 +570,7 @@ export class SnippetSession { let offset = 0; for (let i = 0; i < snippetEdits.length; i++) { - const { range, template } = snippetEdits[i]; + const { range, template, keepWhitespace } = snippetEdits[i]; // gaps between snippet edits are appended as text nodes. this // ensures placeholder-offsets are later correct @@ -582,7 +583,7 @@ export class SnippetSession { } const newNodes = parser.parseFragment(template, snippet); - SnippetSession.adjustWhitespace(model, range.getStartPosition(), true, snippet, new Set(newNodes)); + SnippetSession.adjustWhitespace(model, range.getStartPosition(), keepWhitespace !== undefined ? !keepWhitespace : adjustWhitespace, snippet, new Set(newNodes)); snippet.resolveVariables(resolver); const snippetText = snippet.toString(); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 1968fac5a562b..e599a32a826d5 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -7979,6 +7979,7 @@ declare namespace monaco.languages { resource: Uri; textEdit: TextEdit & { insertAsSnippet?: boolean; + keepWhitespace?: boolean; }; versionId: number | undefined; metadata?: WorkspaceEditMetadata; diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index b47f1a17cb4f8..d569ef6547799 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -1164,13 +1164,15 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic this.workspacesHistoryMainService.removeRecentlyOpened([fileUri]); // assume this is a file that does not yet exist - if (options.ignoreFileNotFound) { + if (options.ignoreFileNotFound && error.code === 'ENOENT') { return { fileUri, type: FileType.File, exists: false }; } + + this.logService.error(`Invalid path provided: ${path}, ${error.message}`); } return undefined; diff --git a/src/vs/workbench/api/browser/mainThreadEditor.ts b/src/vs/workbench/api/browser/mainThreadEditor.ts index 7dd21444995d3..257e99854526a 100644 --- a/src/vs/workbench/api/browser/mainThreadEditor.ts +++ b/src/vs/workbench/api/browser/mainThreadEditor.ts @@ -14,7 +14,7 @@ import { ITextModel, ITextModelUpdateOptions } from '../../../editor/common/mode import { ISingleEditOperation } from '../../../editor/common/core/editOperation.js'; import { IModelService } from '../../../editor/common/services/model.js'; import { SnippetController2 } from '../../../editor/contrib/snippet/browser/snippetController2.js'; -import { IApplyEditsOptions, IEditorPropertiesChangeData, IResolvedTextEditorConfiguration, ITextEditorConfigurationUpdate, IUndoStopOptions, TextEditorRevealType } from '../common/extHost.protocol.js'; +import { IApplyEditsOptions, IEditorPropertiesChangeData, IResolvedTextEditorConfiguration, ISnippetOptions, ITextEditorConfigurationUpdate, TextEditorRevealType } from '../common/extHost.protocol.js'; import { IEditorPane } from '../../common/editor.js'; import { equals } from '../../../base/common/arrays.js'; import { CodeEditorStateFlag, EditorState } from '../../../editor/contrib/editorState/browser/editorState.js'; @@ -509,7 +509,7 @@ export class MainThreadTextEditor { return true; } - async insertSnippet(modelVersionId: number, template: string, ranges: readonly IRange[], opts: IUndoStopOptions) { + async insertSnippet(modelVersionId: number, template: string, ranges: readonly IRange[], opts: ISnippetOptions) { if (!this._codeEditor || !this._codeEditor.hasModel()) { return false; @@ -542,6 +542,7 @@ export class MainThreadTextEditor { snippetController.apply(edits, { overwriteBefore: 0, overwriteAfter: 0, undoStopBefore: opts.undoStopBefore, undoStopAfter: opts.undoStopAfter, + adjustWhitespace: !opts.keepWhitespace, clipboardText }); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 5072aac5a208f..98b0bba761e39 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -261,6 +261,9 @@ export interface IApplyEditsOptions extends IUndoStopOptions { setEndOfLine?: EndOfLineSequence; } +export interface ISnippetOptions extends IUndoStopOptions { + keepWhitespace?: boolean; +} export interface ITextDocumentShowOptions { position?: EditorGroupColumn; preserveFocus?: boolean; diff --git a/src/vs/workbench/api/common/extHostTextEditor.ts b/src/vs/workbench/api/common/extHostTextEditor.ts index 0803130ffbd9a..86a322348a8d2 100644 --- a/src/vs/workbench/api/common/extHostTextEditor.ts +++ b/src/vs/workbench/api/common/extHostTextEditor.ts @@ -495,7 +495,7 @@ export class ExtHostTextEditor { return that._applyEdit(edit); }, // --- snippet edit - insertSnippet(snippet: SnippetString, where?: Position | readonly Position[] | Range | readonly Range[], options: { undoStopBefore: boolean; undoStopAfter: boolean } = { undoStopBefore: true, undoStopAfter: true }): Promise { + insertSnippet(snippet: SnippetString, where?: Position | readonly Position[] | Range | readonly Range[], options: { undoStopBefore: boolean; undoStopAfter: boolean; keepWhitespace?: boolean } = { undoStopBefore: true, undoStopAfter: true }): Promise { if (that._disposed) { return Promise.reject(new Error('TextEditor#insertSnippet not possible on closed editors')); } @@ -521,6 +521,9 @@ export class ExtHostTextEditor { } } } + if (options.keepWhitespace === undefined) { + options.keepWhitespace = false; + } return _proxy.$tryInsertSnippet(id, document.value.version, snippet.value, ranges, options); }, setDecorations(decorationType: vscode.TextEditorDecorationType, ranges: Range[] | vscode.DecorationOptions[]): void { diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index f535d17683ba2..7613e42da8d12 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -657,7 +657,8 @@ export namespace WorkspaceEdit { textEdit: { range: Range.from(entry.range), text: entry.edit.value, - insertAsSnippet: true + insertAsSnippet: true, + keepWhitespace: entry.keepWhitespace }, versionId: !toCreate.has(entry.uri) ? versionInfo?.getTextDocumentVersion(entry.uri) : undefined, metadata: entry.metadata diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index ad3bfb390e6c9..a3e72727d03c8 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -766,6 +766,8 @@ export class SnippetTextEdit implements vscode.SnippetTextEdit { snippet: SnippetString; + keepWhitespace?: boolean; + constructor(range: Range, snippet: SnippetString) { this.range = range; this.snippet = snippet; @@ -809,6 +811,7 @@ export interface IFileSnippetTextEdit { readonly range: vscode.Range; readonly edit: vscode.SnippetString; readonly metadata?: vscode.WorkspaceEditEntryMetadata; + readonly keepWhitespace?: boolean; } export interface IFileCellEdit { @@ -938,7 +941,7 @@ export class WorkspaceEdit implements vscode.WorkspaceEdit { this.replaceNotebookCells(uri, edit.range, edit.newCells, metadata); } } else if (SnippetTextEdit.isSnippetTextEdit(edit)) { - this._edits.push({ _type: FileEditType.Snippet, uri, range: edit.range, edit: edit.snippet, metadata }); + this._edits.push({ _type: FileEditType.Snippet, uri, range: edit.range, edit: edit.snippet, metadata, keepWhitespace: edit.keepWhitespace }); } else { this._edits.push({ _type: FileEditType.Text, uri, edit, metadata }); diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts index 269d2bccfe1ff..4e4ec817f8bde 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts @@ -25,7 +25,7 @@ import { ISnippetEdit } from '../../../../editor/contrib/snippet/browser/snippet type ValidationResult = { canApply: true } | { canApply: false; reason: URI }; -type ISingleSnippetEditOperation = ISingleEditOperation & { insertAsSnippet?: boolean }; +type ISingleSnippetEditOperation = ISingleEditOperation & { insertAsSnippet?: boolean; keepWhitespace?: boolean }; class ModelEditTask implements IDisposable { @@ -80,7 +80,7 @@ class ModelEditTask implements IDisposable { } else { range = Range.lift(textEdit.range); } - this._edits.push({ ...EditOperation.replaceMove(range, textEdit.text), insertAsSnippet: textEdit.insertAsSnippet }); + this._edits.push({ ...EditOperation.replaceMove(range, textEdit.text), insertAsSnippet: textEdit.insertAsSnippet, keepWhitespace: textEdit.keepWhitespace }); } validate(): ValidationResult { @@ -152,7 +152,8 @@ class EditorEditTask extends ModelEditTask { if (edit.range && edit.text !== null) { snippetEdits.push({ range: Range.lift(edit.range), - template: edit.insertAsSnippet ? edit.text : SnippetParser.escape(edit.text) + template: edit.insertAsSnippet ? edit.text : SnippetParser.escape(edit.text), + keepWhitespace: edit.keepWhitespace }); } } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts index 5ed43c1ba4334..bc485108df656 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts @@ -79,7 +79,7 @@ export function getAccessibilityHelpText(type: 'panelChat' | 'inlineChat' | 'qui content.push(localize('chatEditing.expectation', 'When a request is made, a progress indicator will play while the edits are being applied.')); content.push(localize('chatEditing.review', 'Once the edits are applied, focus the editor(s) to review, accept, and discard changes.')); content.push(localize('chatEditing.sections', 'Navigate between edits in the editor with navigate previous{0} and next{1}', '', '')); - content.push(localize('chatEditing.acceptHunk', 'In the editor, Accept{0} and Reject the current Change{1}.', '', '')); + content.push(localize('chatEditing.acceptHunk', 'In the editor, Accept{0}, Reject{1}, or Toggle the Diff{2} for the current Change.', '', '', '')); content.push(localize('chatEditing.helpfulCommands', 'When in the edits view, some helpful commands include:')); content.push(localize('workbench.action.chat.undoEdits', '- Undo Edits{0}.', '')); content.push(localize('workbench.action.chat.editing.attachFiles', '- Attach Files{0}.', '')); diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index 804721590e9f9..67d44daf01f14 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -1312,6 +1312,10 @@ declare module 'vscode' { * Add undo stop after making the edits. */ readonly undoStopAfter: boolean; + /** + * Keep whitespace of the {@link SnippetString.value} as is. + */ + readonly keepWhitespace?: boolean; }): Thenable; /** @@ -3776,6 +3780,11 @@ declare module 'vscode' { */ snippet: SnippetString; + /** + * Whether the snippet edit should be applied with existing whitespace preserved. + */ + keepWhitespace?: boolean; + /** * Create a new snippet edit. *