diff --git a/cli/src/commands/args.rs b/cli/src/commands/args.rs index 11f6e93c6e3..dcdbd808fe7 100644 --- a/cli/src/commands/args.rs +++ b/cli/src/commands/args.rs @@ -662,12 +662,28 @@ pub struct TunnelServeArgs { /// Requests that extensions be preloaded and installed on connecting servers. #[clap(long)] pub install_extension: Vec, + + /// Specifies the directory that server data is kept in. + #[clap(long)] + pub server_data_dir: Option, + + /// Set the root path for extensions. + #[clap(long)] + pub extensions_dir: Option, } impl TunnelServeArgs { pub fn apply_to_server_args(&self, csa: &mut CodeServerArgs) { csa.install_extensions .extend_from_slice(&self.install_extension); + + if let Some(d) = &self.server_data_dir { + csa.server_data_dir = Some(d.clone()); + } + + if let Some(d) = &self.extensions_dir { + csa.extensions_dir = Some(d.clone()); + } } } diff --git a/cli/src/tunnels/code_server.rs b/cli/src/tunnels/code_server.rs index 592ad121291..bb5a9f8d08f 100644 --- a/cli/src/tunnels/code_server.rs +++ b/cli/src/tunnels/code_server.rs @@ -57,6 +57,8 @@ pub struct CodeServerArgs { pub log: Option, pub accept_server_license_terms: bool, pub verbose: bool, + pub server_data_dir: Option, + pub extensions_dir: Option, // extension management pub install_extensions: Vec, pub uninstall_extensions: Vec, @@ -144,6 +146,12 @@ impl CodeServerArgs { args.push(format!("--category={}", i)); } } + if let Some(d) = &self.server_data_dir { + args.push(format!("--server-data-dir={}", d)); + } + if let Some(d) = &self.extensions_dir { + args.push(format!("--extensions-dir={}", d)); + } if self.start_server { args.push(String::from("--start-server")); } diff --git a/extensions/markdown-language-features/package.nls.json b/extensions/markdown-language-features/package.nls.json index 2307358b749..a4b25260ba8 100644 --- a/extensions/markdown-language-features/package.nls.json +++ b/extensions/markdown-language-features/package.nls.json @@ -38,14 +38,14 @@ "configuration.markdown.suggest.paths.includeWorkspaceHeaderCompletions.onDoubleHash": "Enable workspace header suggestions after typing `##` in a path, for example: `[link text](##`.", "configuration.markdown.suggest.paths.includeWorkspaceHeaderCompletions.onSingleOrDoubleHash": "Enable workspace header suggestions after typing either `##` or `#` in a path, for example: `[link text](#` or `[link text](##`.", "configuration.markdown.editor.drop.enabled": "Enable dropping files into a Markdown editor while holding Shift. Requires enabling `#editor.dropIntoEditor.enabled#`.", - "configuration.markdown.editor.drop.always": "Always insert Markdown links.", - "configuration.markdown.editor.drop.smart": "Smartly create Markdown links by default when not dropping into a code block or other special element. Use the drop widget to switch between pasting as plain text or as Markdown links.", - "configuration.markdown.editor.drop.never": "Never create Markdown links.", + "configuration.markdown.editor.drop.enabled.always": "Always insert Markdown links.", + "configuration.markdown.editor.drop.enabled.smart": "Smartly create Markdown links by default when not dropping into a code block or other special element. Use the drop widget to switch between pasting as plain text or as Markdown links.", + "configuration.markdown.editor.drop.enabled.never": "Never create Markdown links.", "configuration.markdown.editor.drop.copyIntoWorkspace": "Controls if files outside of the workspace that are dropped into a Markdown editor should be copied into the workspace.\n\nUse `#markdown.copyFiles.destination#` to configure where copied dropped files should be created", "configuration.markdown.editor.filePaste.enabled": "Enable pasting files into a Markdown editor to create Markdown links. Requires enabling `#editor.pasteAs.enabled#`.", - "configuration.markdown.editor.filePaste.always": "Always insert Markdown links.", - "configuration.markdown.editor.filePaste.smart": "Smartly create Markdown links by default when not pasting into a code block or other special element. Use the paste widget to switch between pasting as plain text or as Markdown links.", - "configuration.markdown.editor.filePaste.never": "Never create Markdown links.", + "configuration.markdown.editor.filePaste.enabled.always": "Always insert Markdown links.", + "configuration.markdown.editor.filePaste.enabled.smart": "Smartly create Markdown links by default when not pasting into a code block or other special element. Use the paste widget to switch between pasting as plain text or as Markdown links.", + "configuration.markdown.editor.filePaste.enabled.never": "Never create Markdown links.", "configuration.markdown.editor.filePaste.copyIntoWorkspace": "Controls if files outside of the workspace that are pasted into a Markdown editor should be copied into the workspace.\n\nUse `#markdown.copyFiles.destination#` to configure where copied files should be created.", "configuration.copyIntoWorkspace.mediaFiles": "Try to copy external image and video files into the workspace.", "configuration.copyIntoWorkspace.never": "Do not copy external files into the workspace.", diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index 5ea2340b270..1c54b960a91 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -65,6 +65,7 @@ "contributes": { "chatParticipants": [ { + "id": "api-test.participant", "name": "participant", "description": "test", "isDefault": true, diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts index 1a07b475074..f2e28006ced 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts @@ -30,7 +30,7 @@ suite('chat', () => { function setupParticipant(): Event<{ request: ChatRequest; context: ChatContext }> { const emitter = new EventEmitter<{ request: ChatRequest; context: ChatContext }>(); - disposables.push(); + disposables.push(emitter); disposables.push(interactive.registerInteractiveSessionProvider('provider', { prepareSession: (_token: CancellationToken): ProviderResult => { return { @@ -40,7 +40,7 @@ suite('chat', () => { }, })); - const participant = chat.createChatParticipant('participant', (request, context, _progress, _token) => { + const participant = chat.createChatParticipant('api-test.participant', (request, context, _progress, _token) => { emitter.fire({ request, context }); return null; }); @@ -49,12 +49,12 @@ suite('chat', () => { return emitter.event; } - test('participant and slash command', async () => { + test('participant and slash command history', async () => { const onRequest = setupParticipant(); commands.executeCommand('workbench.action.chat.open', { query: '@participant /hello friend' }); let i = 0; - onRequest(request => { + disposables.push(onRequest(request => { if (i === 0) { assert.deepStrictEqual(request.request.command, 'hello'); assert.strictEqual(request.request.prompt, 'friend'); @@ -62,10 +62,10 @@ suite('chat', () => { commands.executeCommand('workbench.action.chat.open', { query: '@participant /hello friend' }); } else { assert.strictEqual(request.context.history.length, 1); - assert.strictEqual(request.context.history[0].participant.name, 'participant'); + assert.strictEqual(request.context.history[0].participant, 'api-test.participant'); assert.strictEqual(request.context.history[0].command, 'hello'); } - }); + })); }); test('participant and variable', async () => { @@ -93,7 +93,7 @@ suite('chat', () => { })); const deferred = new DeferredPromise(); - const participant = chat.createChatParticipant('participant', (_request, _context, _progress, _token) => { + const participant = chat.createChatParticipant('api-test.participant', (_request, _context, _progress, _token) => { return { metadata: { key: 'value' } }; }); participant.isDefault = true; diff --git a/package.json b/package.json index 25ab5b12dc6..aaad3cf8140 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.88.0", - "distro": "ff3bff60edcc6e1f7269509e1673036c00fa62bd", + "distro": "7734ec27c8ec09ddc68bc3618e17d9a8b40fbfd9", "author": { "name": "Microsoft Corporation" }, diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf index 25c7f25a69b..57eda48f1db 100644 Binary files a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf and b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf differ diff --git a/src/vs/base/browser/ui/inputbox/inputBox.ts b/src/vs/base/browser/ui/inputbox/inputBox.ts index 0a06ece485f..b899215c98f 100644 --- a/src/vs/base/browser/ui/inputbox/inputBox.ts +++ b/src/vs/base/browser/ui/inputbox/inputBox.ts @@ -312,6 +312,18 @@ export class InputBox extends Widget { return this.input.selectionEnd === this.input.value.length && this.input.selectionStart === this.input.selectionEnd; } + public getSelection(): IRange | null { + const selectionStart = this.input.selectionStart; + if (selectionStart === null) { + return null; + } + const selectionEnd = this.input.selectionEnd ?? selectionStart; + return { + start: selectionStart, + end: selectionEnd, + }; + } + public enable(): void { this.input.removeAttribute('disabled'); } diff --git a/src/vs/base/browser/ui/toggle/toggle.ts b/src/vs/base/browser/ui/toggle/toggle.ts index 22d60e98508..9e70dbdbe6f 100644 --- a/src/vs/base/browser/ui/toggle/toggle.ts +++ b/src/vs/base/browser/ui/toggle/toggle.ts @@ -237,7 +237,7 @@ export class Checkbox extends Widget { constructor(private title: string, private isChecked: boolean, styles: ICheckboxStyles) { super(); - this.checkbox = new Toggle({ title: this.title, isChecked: this.isChecked, icon: Codicon.check, actionClassName: 'monaco-checkbox', ...unthemedToggleStyles }); + this.checkbox = this._register(new Toggle({ title: this.title, isChecked: this.isChecked, icon: Codicon.check, actionClassName: 'monaco-checkbox', ...unthemedToggleStyles })); this.domNode = this.checkbox.domNode; diff --git a/src/vs/base/common/codiconsLibrary.ts b/src/vs/base/common/codiconsLibrary.ts index a3e87b12ef7..c3528c268b9 100644 --- a/src/vs/base/common/codiconsLibrary.ts +++ b/src/vs/base/common/codiconsLibrary.ts @@ -525,7 +525,11 @@ export const codiconsLibrary = { blank: register('blank', 0xec03), heartFilled: register('heart-filled', 0xec04), map: register('map', 0xec05), + mapHorizontal: register('map-horizontal', 0xec05), + foldHorizontal: register('fold-horizontal', 0xec05), mapFilled: register('map-filled', 0xec06), + mapHorizontalFilled: register('map-horizontal-filled', 0xec06), + foldHorizontalFilled: register('fold-horizontal-filled', 0xec06), circleSmall: register('circle-small', 0xec07), bellSlash: register('bell-slash', 0xec08), bellSlashDot: register('bell-slash-dot', 0xec09), @@ -567,4 +571,8 @@ export const codiconsLibrary = { runAllCoverage: register('run-all-coverage', 0xec2d), coverage: register('coverage', 0xec2e), githubProject: register('github-project', 0xec2f), + mapVertical: register('map-vertical', 0xec30), + foldVertical: register('fold-vertical', 0xec30), + mapVerticalFilled: register('map-vertical-filled', 0xec31), + foldVerticalFilled: register('fold-vertical-filled', 0xec31), } as const; diff --git a/src/vs/base/node/osDisplayProtocolInfo.ts b/src/vs/base/node/osDisplayProtocolInfo.ts new file mode 100644 index 00000000000..c028dc88536 --- /dev/null +++ b/src/vs/base/node/osDisplayProtocolInfo.ts @@ -0,0 +1,78 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { constants as FSConstants } from 'fs'; +import { access } from 'fs/promises'; +import { join } from 'vs/base/common/path'; +import { env } from 'vs/base/common/process'; + +const XDG_SESSION_TYPE = 'XDG_SESSION_TYPE'; +const WAYLAND_DISPLAY = 'WAYLAND_DISPLAY'; +const XDG_RUNTIME_DIR = 'XDG_RUNTIME_DIR'; + +const enum DisplayProtocolType { + Wayland = 'wayland', + XWayland = 'xwayland', + X11 = 'x11', + Unknown = 'unknown' +} + +export async function getDisplayProtocol(errorLogger: (error: any) => void): Promise { + const xdgSessionType = env[XDG_SESSION_TYPE]; + + if (xdgSessionType) { + // If XDG_SESSION_TYPE is set, return its value if it's either 'wayland' or 'x11'. + // We assume that any value other than 'wayland' or 'x11' is an error or unexpected, + // hence 'unknown' is returned. + return xdgSessionType === DisplayProtocolType.Wayland || xdgSessionType === DisplayProtocolType.X11 ? xdgSessionType : DisplayProtocolType.Unknown; + } else { + const waylandDisplay = env[WAYLAND_DISPLAY]; + + if (!waylandDisplay) { + // If WAYLAND_DISPLAY is empty, then the session is x11. + return DisplayProtocolType.X11; + } else { + const xdgRuntimeDir = env[XDG_RUNTIME_DIR]; + + if (!xdgRuntimeDir) { + // If XDG_RUNTIME_DIR is empty, then the session can only be guessed. + return DisplayProtocolType.Unknown; + } else { + // Check for the presence of the file $XDG_RUNTIME_DIR/wayland-0. + const waylandServerPipe = join(xdgRuntimeDir, 'wayland-0'); + + try { + await access(waylandServerPipe, FSConstants.R_OK); + + // If the file exists, then the session is wayland. + return DisplayProtocolType.Wayland; + } catch (err) { + // If the file does not exist or an error occurs, we guess 'unknown' + // since WAYLAND_DISPLAY was set but no wayland-0 pipe could be confirmed. + errorLogger(err); + return DisplayProtocolType.Unknown; + } + } + } + } +} + + +export function getCodeDisplayProtocol(displayProtocol: DisplayProtocolType, ozonePlatform: string | undefined): DisplayProtocolType { + if (!ozonePlatform) { + return displayProtocol === DisplayProtocolType.Wayland ? DisplayProtocolType.XWayland : DisplayProtocolType.X11; + } else { + switch (ozonePlatform) { + case 'auto': + return displayProtocol; + case 'x11': + return displayProtocol === DisplayProtocolType.Wayland ? DisplayProtocolType.XWayland : DisplayProtocolType.X11; + case 'wayland': + return DisplayProtocolType.Wayland; + default: + return DisplayProtocolType.Unknown; + } + } +} diff --git a/src/vs/code/node/sharedProcess/sharedProcessMain.ts b/src/vs/code/node/sharedProcess/sharedProcessMain.ts index 81faf87e87b..9a59ccce5c3 100644 --- a/src/vs/code/node/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/node/sharedProcess/sharedProcessMain.ts @@ -118,6 +118,7 @@ import { NativeEnvironmentService } from 'vs/platform/environment/node/environme import { SharedProcessRawConnection, SharedProcessLifecycle } from 'vs/platform/sharedProcess/common/sharedProcess'; import { getOSReleaseInfo } from 'vs/base/node/osReleaseInfo'; import { getDesktopEnvironment } from 'vs/base/common/desktopEnvironmentInfo'; +import { getCodeDisplayProtocol, getDisplayProtocol } from 'vs/base/node/osDisplayProtocolInfo'; class SharedProcessMain extends Disposable implements IClientConnectionFilter { @@ -465,14 +466,20 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter { private async reportClientOSInfo(telemetryService: ITelemetryService, logService: ILogService): Promise { if (isLinux) { - const releaseInfo = await getOSReleaseInfo(logService.error.bind(logService)); + const [releaseInfo, displayProtocol] = await Promise.all([ + getOSReleaseInfo(logService.error.bind(logService)), + getDisplayProtocol(logService.error.bind(logService)) + ]); const desktopEnvironment = getDesktopEnvironment(); + const codeSessionType = getCodeDisplayProtocol(displayProtocol, this.configuration.args['ozone-platform']); if (releaseInfo) { type ClientPlatformInfoClassification = { platformId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'A string identifying the operating system without any version information.' }; platformVersionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'A string identifying the operating system version excluding any name information or release code.' }; platformIdLike: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'A string identifying the operating system the current OS derivate is closely related to.' }; desktopEnvironment: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'A string identifying the desktop environment the user is using.' }; + displayProtocol: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'A string identifying the users display protocol type.' }; + codeDisplayProtocol: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'A string identifying the vscode display protocol type.' }; owner: 'benibenj'; comment: 'Provides insight into the distro and desktop environment information on Linux.'; }; @@ -481,12 +488,16 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter { platformVersionId: string | undefined; platformIdLike: string | undefined; desktopEnvironment: string | undefined; + displayProtocol: string | undefined; + codeDisplayProtocol: string | undefined; }; telemetryService.publicLog2('clientPlatformInfo', { platformId: releaseInfo.id, platformVersionId: releaseInfo.version_id, platformIdLike: releaseInfo.id_like, - desktopEnvironment: desktopEnvironment + desktopEnvironment: desktopEnvironment, + displayProtocol: displayProtocol, + codeDisplayProtocol: codeSessionType }); } } diff --git a/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts b/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts index 1338656acab..0ca31065a45 100644 --- a/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts +++ b/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts @@ -10,7 +10,7 @@ import { ViewPart } from 'vs/editor/browser/view/viewPart'; import { Position } from 'vs/editor/common/core/position'; import { IEditorConfiguration } from 'vs/editor/common/config/editorConfiguration'; import { TokenizationRegistry } from 'vs/editor/common/languages'; -import { editorCursorForeground, editorOverviewRulerBorder, editorOverviewRulerBackground } from 'vs/editor/common/core/editorColorRegistry'; +import { editorCursorForeground, editorOverviewRulerBorder, editorOverviewRulerBackground, editorMultiCursorSecondaryForeground, editorMultiCursorPrimaryForeground } from 'vs/editor/common/core/editorColorRegistry'; import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/browser/view/renderingContext'; import { ViewContext } from 'vs/editor/common/viewModel/viewContext'; import { EditorTheme } from 'vs/editor/common/editorTheme'; @@ -29,7 +29,9 @@ class Settings { public readonly borderColor: string | null; public readonly hideCursor: boolean; - public readonly cursorColor: string | null; + public readonly cursorColorSingle: string | null; + public readonly cursorColorPrimary: string | null; + public readonly cursorColorSecondary: string | null; public readonly themeType: 'light' | 'dark' | 'hcLight' | 'hcDark'; public readonly backgroundColor: Color | null; @@ -55,8 +57,12 @@ class Settings { this.borderColor = borderColor ? borderColor.toString() : null; this.hideCursor = options.get(EditorOption.hideCursorInOverviewRuler); - const cursorColor = theme.getColor(editorCursorForeground); - this.cursorColor = cursorColor ? cursorColor.transparent(0.7).toString() : null; + const cursorColorSingle = theme.getColor(editorCursorForeground); + this.cursorColorSingle = cursorColorSingle ? cursorColorSingle.transparent(0.7).toString() : null; + const cursorColorPrimary = theme.getColor(editorMultiCursorPrimaryForeground); + this.cursorColorPrimary = cursorColorPrimary ? cursorColorPrimary.transparent(0.7).toString() : null; + const cursorColorSecondary = theme.getColor(editorMultiCursorSecondaryForeground); + this.cursorColorSecondary = cursorColorSecondary ? cursorColorSecondary.transparent(0.7).toString() : null; this.themeType = theme.type; @@ -189,7 +195,9 @@ class Settings { && this.renderBorder === other.renderBorder && this.borderColor === other.borderColor && this.hideCursor === other.hideCursor - && this.cursorColor === other.cursorColor + && this.cursorColorSingle === other.cursorColorSingle + && this.cursorColorPrimary === other.cursorColorPrimary + && this.cursorColorSecondary === other.cursorColorSecondary && this.themeType === other.themeType && Color.equals(this.backgroundColor, other.backgroundColor) && this.top === other.top @@ -213,6 +221,11 @@ const enum OverviewRulerLane { Full = 7 } +type Cursor = { + position: Position; + color: string | null; +}; + const enum ShouldRenderValue { NotNeeded = 0, Maybe = 1, @@ -226,10 +239,10 @@ export class DecorationsOverviewRuler extends ViewPart { private readonly _tokensColorTrackerListener: IDisposable; private readonly _domNode: FastDomNode; private _settings!: Settings; - private _cursorPositions: Position[]; + private _cursorPositions: Cursor[]; private _renderedDecorations: OverviewRulerDecorationsGroup[] = []; - private _renderedCursorPositions: Position[] = []; + private _renderedCursorPositions: Cursor[] = []; constructor(context: ViewContext) { super(context); @@ -249,7 +262,7 @@ export class DecorationsOverviewRuler extends ViewPart { } }); - this._cursorPositions = [new Position(1, 1)]; + this._cursorPositions = [{ position: new Position(1, 1), color: this._settings.cursorColorSingle }]; } public override dispose(): void { @@ -298,9 +311,13 @@ export class DecorationsOverviewRuler extends ViewPart { public override onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean { this._cursorPositions = []; for (let i = 0, len = e.selections.length; i < len; i++) { - this._cursorPositions[i] = e.selections[i].getPosition(); + let color = this._settings.cursorColorSingle; + if (len > 1) { + color = i === 0 ? this._settings.cursorColorPrimary : this._settings.cursorColorSecondary; + } + this._cursorPositions.push({ position: e.selections[i].getPosition(), color }); } - this._cursorPositions.sort(Position.compare); + this._cursorPositions.sort((a, b) => Position.compare(a.position, b.position)); return this._markRenderingIsMaybeNeeded(); } public override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean { @@ -352,7 +369,7 @@ export class DecorationsOverviewRuler extends ViewPart { if (this._actualShouldRender === ShouldRenderValue.Maybe && !OverviewRulerDecorationsGroup.equalsArr(this._renderedDecorations, decorations)) { this._actualShouldRender = ShouldRenderValue.Needed; } - if (this._actualShouldRender === ShouldRenderValue.Maybe && !equals(this._renderedCursorPositions, this._cursorPositions, (a, b) => a.lineNumber === b.lineNumber)) { + if (this._actualShouldRender === ShouldRenderValue.Maybe && !equals(this._renderedCursorPositions, this._cursorPositions, (a, b) => a.position.lineNumber === b.position.lineNumber && a.color === b.color)) { this._actualShouldRender = ShouldRenderValue.Needed; } if (this._actualShouldRender === ShouldRenderValue.Maybe) { @@ -443,17 +460,21 @@ export class DecorationsOverviewRuler extends ViewPart { } // Draw cursors - if (!this._settings.hideCursor && this._settings.cursorColor) { + if (!this._settings.hideCursor) { const cursorHeight = (2 * this._settings.pixelRatio) | 0; const halfCursorHeight = (cursorHeight / 2) | 0; const cursorX = this._settings.x[OverviewRulerLane.Full]; const cursorW = this._settings.w[OverviewRulerLane.Full]; - canvasCtx.fillStyle = this._settings.cursorColor; let prevY1 = -100; let prevY2 = -100; + let prevColor: string | null = null; for (let i = 0, len = this._cursorPositions.length; i < len; i++) { - const cursor = this._cursorPositions[i]; + const color = this._cursorPositions[i].color; + if (!color) { + continue; + } + const cursor = this._cursorPositions[i].position; let yCenter = (viewLayout.getVerticalOffsetForLineNumber(cursor.lineNumber) * heightRatio) | 0; if (yCenter < halfCursorHeight) { @@ -464,9 +485,9 @@ export class DecorationsOverviewRuler extends ViewPart { const y1 = yCenter - halfCursorHeight; const y2 = y1 + cursorHeight; - if (y1 > prevY2 + 1) { + if (y1 > prevY2 + 1 || color !== prevColor) { // flush prev - if (i !== 0) { + if (i !== 0 && prevColor) { canvasCtx.fillRect(cursorX, prevY1, cursorW, prevY2 - prevY1); } prevY1 = y1; @@ -477,8 +498,12 @@ export class DecorationsOverviewRuler extends ViewPart { prevY2 = y2; } } + prevColor = color; + canvasCtx.fillStyle = color; + } + if (prevColor) { + canvasCtx.fillRect(cursorX, prevY1, cursorW, prevY2 - prevY1); } - canvasCtx.fillRect(cursorX, prevY1, cursorW, prevY2 - prevY1); } if (this._settings.renderBorder && this._settings.borderColor && this._settings.overviewRulerLanes > 0) { diff --git a/src/vs/editor/browser/viewParts/viewCursors/viewCursor.ts b/src/vs/editor/browser/viewParts/viewCursors/viewCursor.ts index 36506e9fed0..4502698cfc5 100644 --- a/src/vs/editor/browser/viewParts/viewCursors/viewCursor.ts +++ b/src/vs/editor/browser/viewParts/viewCursors/viewCursor.ts @@ -35,6 +35,12 @@ class ViewCursorRenderData { ) { } } +export enum CursorPlurality { + Single, + MultiPrimary, + MultiSecondary, +} + export class ViewCursor { private readonly _context: ViewContext; private readonly _domNode: FastDomNode; @@ -47,11 +53,12 @@ export class ViewCursor { private _isVisible: boolean; private _position: Position; + private _pluralityClass: string; private _lastRenderedContent: string; private _renderData: ViewCursorRenderData | null; - constructor(context: ViewContext) { + constructor(context: ViewContext, plurality: CursorPlurality) { this._context = context; const options = this._context.configuration.options; const fontInfo = options.get(EditorOption.fontInfo); @@ -73,6 +80,8 @@ export class ViewCursor { this._domNode.setDisplay('none'); this._position = new Position(1, 1); + this._pluralityClass = ''; + this.setPlurality(plurality); this._lastRenderedContent = ''; this._renderData = null; @@ -86,6 +95,23 @@ export class ViewCursor { return this._position; } + public setPlurality(plurality: CursorPlurality) { + switch (plurality) { + default: + case CursorPlurality.Single: + this._pluralityClass = ''; + break; + + case CursorPlurality.MultiPrimary: + this._pluralityClass = 'cursor-primary'; + break; + + case CursorPlurality.MultiSecondary: + this._pluralityClass = 'cursor-secondary'; + break; + } + } + public show(): void { if (!this._isVisible) { this._domNode.setVisibility('inherit'); @@ -229,7 +255,7 @@ export class ViewCursor { this._domNode.domNode.textContent = this._lastRenderedContent; } - this._domNode.setClassName(`cursor ${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME} ${this._renderData.textContentClassName}`); + this._domNode.setClassName(`cursor ${this._pluralityClass} ${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME} ${this._renderData.textContentClassName}`); this._domNode.setDisplay('block'); this._domNode.setTop(this._renderData.top); diff --git a/src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts b/src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts index 1be969b7497..ceb27ce5ea3 100644 --- a/src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts +++ b/src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts @@ -7,10 +7,14 @@ import 'vs/css!./viewCursors'; import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; import { IntervalTimer, TimeoutTimer } from 'vs/base/common/async'; import { ViewPart } from 'vs/editor/browser/view/viewPart'; -import { IViewCursorRenderData, ViewCursor } from 'vs/editor/browser/viewParts/viewCursors/viewCursor'; +import { IViewCursorRenderData, ViewCursor, CursorPlurality } from 'vs/editor/browser/viewParts/viewCursors/viewCursor'; import { TextEditorCursorBlinkingStyle, TextEditorCursorStyle, EditorOption } from 'vs/editor/common/config/editorOptions'; import { Position } from 'vs/editor/common/core/position'; -import { editorCursorBackground, editorCursorForeground } from 'vs/editor/common/core/editorColorRegistry'; +import { + editorCursorBackground, editorCursorForeground, + editorMultiCursorPrimaryForeground, editorMultiCursorPrimaryBackground, + editorMultiCursorSecondaryForeground, editorMultiCursorSecondaryBackground +} from 'vs/editor/common/core/editorColorRegistry'; import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/browser/view/renderingContext'; import { ViewContext } from 'vs/editor/common/viewModel/viewContext'; import * as viewEvents from 'vs/editor/common/viewEvents'; @@ -57,7 +61,7 @@ export class ViewCursors extends ViewPart { this._isVisible = false; - this._primaryCursor = new ViewCursor(this._context); + this._primaryCursor = new ViewCursor(this._context, CursorPlurality.Single); this._secondaryCursors = []; this._renderData = []; @@ -88,6 +92,7 @@ export class ViewCursors extends ViewPart { } // --- begin event handlers + public override onCompositionStart(e: viewEvents.ViewCompositionStartEvent): boolean { this._isComposingInput = true; this._updateBlinking(); @@ -120,6 +125,7 @@ export class ViewCursors extends ViewPart { this._secondaryCursors.length !== secondaryPositions.length || (this._cursorSmoothCaretAnimation === 'explicit' && reason !== CursorChangeReason.Explicit) ); + this._primaryCursor.setPlurality(secondaryPositions.length ? CursorPlurality.MultiPrimary : CursorPlurality.Single); this._primaryCursor.onCursorPositionChanged(position, pauseAnimation); this._updateBlinking(); @@ -127,7 +133,7 @@ export class ViewCursors extends ViewPart { // Create new cursors const addCnt = secondaryPositions.length - this._secondaryCursors.length; for (let i = 0; i < addCnt; i++) { - const newCursor = new ViewCursor(this._context); + const newCursor = new ViewCursor(this._context, CursorPlurality.MultiSecondary); this._domNode.domNode.insertBefore(newCursor.getDomNode().domNode, this._primaryCursor.getDomNode().domNode.nextSibling); this._secondaryCursors.push(newCursor); } @@ -160,7 +166,6 @@ export class ViewCursors extends ViewPart { return true; } - public override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean { // true for inline decorations that can end up relayouting text return true; @@ -263,6 +268,7 @@ export class ViewCursors extends ViewPart { } } } + // --- end blinking logic private _updateDomClassName(): void { @@ -375,16 +381,29 @@ export class ViewCursors extends ViewPart { } registerThemingParticipant((theme, collector) => { - const caret = theme.getColor(editorCursorForeground); - if (caret) { - let caretBackground = theme.getColor(editorCursorBackground); - if (!caretBackground) { - caretBackground = caret.opposite(); - } - collector.addRule(`.monaco-editor .cursors-layer .cursor { background-color: ${caret}; border-color: ${caret}; color: ${caretBackground}; }`); - if (isHighContrast(theme.type)) { - collector.addRule(`.monaco-editor .cursors-layer.has-selection .cursor { border-left: 1px solid ${caretBackground}; border-right: 1px solid ${caretBackground}; }`); + type CursorTheme = { + foreground: string; + background: string; + class: string; + }; + + const cursorThemes: CursorTheme[] = [ + { class: '.cursor', foreground: editorCursorForeground, background: editorCursorBackground }, + { class: '.cursor-primary', foreground: editorMultiCursorPrimaryForeground, background: editorMultiCursorPrimaryBackground }, + { class: '.cursor-secondary', foreground: editorMultiCursorSecondaryForeground, background: editorMultiCursorSecondaryBackground }, + ]; + + for (const cursorTheme of cursorThemes) { + const caret = theme.getColor(cursorTheme.foreground); + if (caret) { + let caretBackground = theme.getColor(cursorTheme.background); + if (!caretBackground) { + caretBackground = caret.opposite(); + } + collector.addRule(`.monaco-editor .cursors-layer ${cursorTheme.class} { background-color: ${caret}; border-color: ${caret}; color: ${caretBackground}; }`); + if (isHighContrast(theme.type)) { + collector.addRule(`.monaco-editor .cursors-layer.has-selection ${cursorTheme.class} { border-left: 1px solid ${caretBackground}; border-right: 1px solid ${caretBackground}; }`); + } } } - }); diff --git a/src/vs/editor/common/core/editorColorRegistry.ts b/src/vs/editor/common/core/editorColorRegistry.ts index 95b738b7b6f..88e0419268f 100644 --- a/src/vs/editor/common/core/editorColorRegistry.ts +++ b/src/vs/editor/common/core/editorColorRegistry.ts @@ -20,6 +20,10 @@ export const editorSymbolHighlightBorder = registerColor('editor.symbolHighlight export const editorCursorForeground = registerColor('editorCursor.foreground', { dark: '#AEAFAD', light: Color.black, hcDark: Color.white, hcLight: '#0F4A85' }, nls.localize('caret', 'Color of the editor cursor.')); export const editorCursorBackground = registerColor('editorCursor.background', null, nls.localize('editorCursorBackground', 'The background color of the editor cursor. Allows customizing the color of a character overlapped by a block cursor.')); +export const editorMultiCursorPrimaryForeground = registerColor('editorMultiCursor.primary.foreground', { dark: editorCursorForeground, light: editorCursorForeground, hcDark: editorCursorForeground, hcLight: editorCursorForeground }, nls.localize('editorMultiCursorPrimaryForeground', 'Color of the primary editor cursor when multiple cursors are present.')); +export const editorMultiCursorPrimaryBackground = registerColor('editorMultiCursor.primary.background', { dark: editorCursorBackground, light: editorCursorBackground, hcDark: editorCursorBackground, hcLight: editorCursorBackground }, nls.localize('editorMultiCursorPrimaryBackground', 'The background color of the primary editor cursor when multiple cursors are present. Allows customizing the color of a character overlapped by a block cursor.')); +export const editorMultiCursorSecondaryForeground = registerColor('editorMultiCursor.secondary.foreground', { dark: editorCursorForeground, light: editorCursorForeground, hcDark: editorCursorForeground, hcLight: editorCursorForeground }, nls.localize('editorMultiCursorSecondaryForeground', 'Color of secondary editor cursors when multiple cursors are present.')); +export const editorMultiCursorSecondaryBackground = registerColor('editorMultiCursor.secondary.background', { dark: editorCursorBackground, light: editorCursorBackground, hcDark: editorCursorBackground, hcLight: editorCursorBackground }, nls.localize('editorMultiCursorSecondaryBackground', 'The background color of secondary editor cursors when multiple cursors are present. Allows customizing the color of a character overlapped by a block cursor.')); export const editorWhitespaces = registerColor('editorWhitespace.foreground', { dark: '#e3e4e229', light: '#33333333', hcDark: '#e3e4e229', hcLight: '#CCCCCC' }, nls.localize('editorWhitespaces', 'Color of whitespace characters in the editor.')); export const editorLineNumbers = registerColor('editorLineNumber.foreground', { dark: '#858585', light: '#237893', hcDark: Color.white, hcLight: '#292929' }, nls.localize('editorLineNumbers', 'Color of editor line numbers.')); diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionController.ts b/src/vs/editor/contrib/codeAction/browser/codeActionController.ts index 5511dde958b..784be6bebee 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionController.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeActionController.ts @@ -36,8 +36,9 @@ import { IEditorProgressService } from 'vs/platform/progress/common/progress'; import { editorFindMatchHighlight, editorFindMatchHighlightBorder } from 'vs/platform/theme/common/colorRegistry'; import { isHighContrast } from 'vs/platform/theme/common/theme'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { CodeActionAutoApply, CodeActionFilter, CodeActionItem, CodeActionSet, CodeActionTrigger, CodeActionTriggerSource } from '../common/types'; -import { CodeActionModel, CodeActionsState } from './codeActionModel'; +import { CodeActionAutoApply, CodeActionFilter, CodeActionItem, CodeActionKind, CodeActionSet, CodeActionTrigger, CodeActionTriggerSource } from 'vs/editor/contrib/codeAction/common/types'; +import { CodeActionModel, CodeActionsState } from 'vs/editor/contrib/codeAction/browser/codeActionModel'; +import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; interface IActionShowOptions { @@ -291,7 +292,22 @@ export class CodeActionController extends Disposable implements IEditorContribut if (token.isCancellationRequested) { return; } - return { canPreview: !!action.action.edit?.edits.length }; + + let canPreview = false; + const actionKind = action.action.kind; + + if (actionKind) { + const hierarchicalKind = new HierarchicalKind(actionKind); + const refactorKinds = [ + CodeActionKind.RefactorExtract, + CodeActionKind.RefactorInline, + CodeActionKind.RefactorRewrite + ]; + + canPreview = refactorKinds.some(refactorKind => refactorKind.contains(hierarchicalKind)); + } + + return { canPreview: canPreview || !!action.action.edit?.edits.length }; }, onFocus: (action: CodeActionItem | undefined) => { if (action && action.action) { diff --git a/src/vs/editor/contrib/indentation/browser/indentation.ts b/src/vs/editor/contrib/indentation/browser/indentation.ts index a14b7d6d5ae..1db0b5adb34 100644 --- a/src/vs/editor/contrib/indentation/browser/indentation.ts +++ b/src/vs/editor/contrib/indentation/browser/indentation.ts @@ -9,7 +9,7 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction, EditorContributionInstantiation, IActionOptions, registerEditorAction, registerEditorContribution, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { ShiftCommand } from 'vs/editor/common/commands/shiftCommand'; import { EditorAutoIndentStrategy, EditorOption } from 'vs/editor/common/config/editorOptions'; -import { EditOperation, ISingleEditOperation } from 'vs/editor/common/core/editOperation'; +import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { IRange, Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { ICommand, ICursorStateComputerData, IEditOperationBuilder, IEditorContribution } from 'vs/editor/common/editorCommon'; @@ -20,123 +20,11 @@ import { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { IndentConsts } from 'vs/editor/common/languages/supports/indentRules'; import { IModelService } from 'vs/editor/common/services/model'; -import * as indentUtils from 'vs/editor/contrib/indentation/browser/indentUtils'; +import * as indentUtils from 'vs/editor/contrib/indentation/common/indentUtils'; import * as nls from 'vs/nls'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; -import { normalizeIndentation } from 'vs/editor/common/core/indentation'; import { getGoodIndentForLine, getIndentMetadata } from 'vs/editor/common/languages/autoIndent'; - -export function getReindentEditOperations(model: ITextModel, languageConfigurationService: ILanguageConfigurationService, startLineNumber: number, endLineNumber: number, inheritedIndent?: string): ISingleEditOperation[] { - if (model.getLineCount() === 1 && model.getLineMaxColumn(1) === 1) { - // Model is empty - return []; - } - - const indentationRules = languageConfigurationService.getLanguageConfiguration(model.getLanguageId()).indentationRules; - if (!indentationRules) { - return []; - } - - endLineNumber = Math.min(endLineNumber, model.getLineCount()); - - // Skip `unIndentedLinePattern` lines - while (startLineNumber <= endLineNumber) { - if (!indentationRules.unIndentedLinePattern) { - break; - } - - const text = model.getLineContent(startLineNumber); - if (!indentationRules.unIndentedLinePattern.test(text)) { - break; - } - - startLineNumber++; - } - - if (startLineNumber > endLineNumber - 1) { - return []; - } - - const { tabSize, indentSize, insertSpaces } = model.getOptions(); - const shiftIndent = (indentation: string, count?: number) => { - count = count || 1; - return ShiftCommand.shiftIndent(indentation, indentation.length + count, tabSize, indentSize, insertSpaces); - }; - const unshiftIndent = (indentation: string, count?: number) => { - count = count || 1; - return ShiftCommand.unshiftIndent(indentation, indentation.length + count, tabSize, indentSize, insertSpaces); - }; - const indentEdits: ISingleEditOperation[] = []; - - // indentation being passed to lines below - let globalIndent: string; - - // Calculate indentation for the first line - // If there is no passed-in indentation, we use the indentation of the first line as base. - const currentLineText = model.getLineContent(startLineNumber); - let adjustedLineContent = currentLineText; - if (inheritedIndent !== undefined && inheritedIndent !== null) { - globalIndent = inheritedIndent; - const oldIndentation = strings.getLeadingWhitespace(currentLineText); - - adjustedLineContent = globalIndent + currentLineText.substring(oldIndentation.length); - if (indentationRules.decreaseIndentPattern && indentationRules.decreaseIndentPattern.test(adjustedLineContent)) { - globalIndent = unshiftIndent(globalIndent); - adjustedLineContent = globalIndent + currentLineText.substring(oldIndentation.length); - - } - if (currentLineText !== adjustedLineContent) { - indentEdits.push(EditOperation.replaceMove(new Selection(startLineNumber, 1, startLineNumber, oldIndentation.length + 1), normalizeIndentation(globalIndent, indentSize, insertSpaces))); - } - } else { - globalIndent = strings.getLeadingWhitespace(currentLineText); - } - - // idealIndentForNextLine doesn't equal globalIndent when there is a line matching `indentNextLinePattern`. - let idealIndentForNextLine: string = globalIndent; - - if (indentationRules.increaseIndentPattern && indentationRules.increaseIndentPattern.test(adjustedLineContent)) { - idealIndentForNextLine = shiftIndent(idealIndentForNextLine); - globalIndent = shiftIndent(globalIndent); - } - else if (indentationRules.indentNextLinePattern && indentationRules.indentNextLinePattern.test(adjustedLineContent)) { - idealIndentForNextLine = shiftIndent(idealIndentForNextLine); - } - - startLineNumber++; - - // Calculate indentation adjustment for all following lines - for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) { - const text = model.getLineContent(lineNumber); - const oldIndentation = strings.getLeadingWhitespace(text); - const adjustedLineContent = idealIndentForNextLine + text.substring(oldIndentation.length); - - if (indentationRules.decreaseIndentPattern && indentationRules.decreaseIndentPattern.test(adjustedLineContent)) { - idealIndentForNextLine = unshiftIndent(idealIndentForNextLine); - globalIndent = unshiftIndent(globalIndent); - } - - if (oldIndentation !== idealIndentForNextLine) { - indentEdits.push(EditOperation.replaceMove(new Selection(lineNumber, 1, lineNumber, oldIndentation.length + 1), normalizeIndentation(idealIndentForNextLine, indentSize, insertSpaces))); - } - - // calculate idealIndentForNextLine - if (indentationRules.unIndentedLinePattern && indentationRules.unIndentedLinePattern.test(text)) { - // In reindent phase, if the line matches `unIndentedLinePattern` we inherit indentation from above lines - // but don't change globalIndent and idealIndentForNextLine. - continue; - } else if (indentationRules.increaseIndentPattern && indentationRules.increaseIndentPattern.test(adjustedLineContent)) { - globalIndent = shiftIndent(globalIndent); - idealIndentForNextLine = globalIndent; - } else if (indentationRules.indentNextLinePattern && indentationRules.indentNextLinePattern.test(adjustedLineContent)) { - idealIndentForNextLine = shiftIndent(idealIndentForNextLine); - } else { - idealIndentForNextLine = globalIndent; - } - } - - return indentEdits; -} +import { getReindentEditOperations } from '../common/indentation'; export class IndentationToSpacesAction extends EditorAction { public static readonly ID = 'editor.action.indentationToSpaces'; diff --git a/src/vs/editor/contrib/indentation/browser/indentUtils.ts b/src/vs/editor/contrib/indentation/common/indentUtils.ts similarity index 100% rename from src/vs/editor/contrib/indentation/browser/indentUtils.ts rename to src/vs/editor/contrib/indentation/common/indentUtils.ts diff --git a/src/vs/editor/contrib/indentation/common/indentation.ts b/src/vs/editor/contrib/indentation/common/indentation.ts new file mode 100644 index 00000000000..760a14919fb --- /dev/null +++ b/src/vs/editor/contrib/indentation/common/indentation.ts @@ -0,0 +1,124 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as strings from 'vs/base/common/strings'; +import { ShiftCommand } from 'vs/editor/common/commands/shiftCommand'; +import { EditOperation, ISingleEditOperation } from 'vs/editor/common/core/editOperation'; +import { normalizeIndentation } from 'vs/editor/common/core/indentation'; +import { Selection } from 'vs/editor/common/core/selection'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { ITextModel } from 'vs/editor/common/model'; + +export function getReindentEditOperations(model: ITextModel, languageConfigurationService: ILanguageConfigurationService, startLineNumber: number, endLineNumber: number, inheritedIndent?: string): ISingleEditOperation[] { + if (model.getLineCount() === 1 && model.getLineMaxColumn(1) === 1) { + // Model is empty + return []; + } + + const indentationRules = languageConfigurationService.getLanguageConfiguration(model.getLanguageId()).indentationRules; + if (!indentationRules) { + return []; + } + + endLineNumber = Math.min(endLineNumber, model.getLineCount()); + + // Skip `unIndentedLinePattern` lines + while (startLineNumber <= endLineNumber) { + if (!indentationRules.unIndentedLinePattern) { + break; + } + + const text = model.getLineContent(startLineNumber); + if (!indentationRules.unIndentedLinePattern.test(text)) { + break; + } + + startLineNumber++; + } + + if (startLineNumber > endLineNumber - 1) { + return []; + } + + const { tabSize, indentSize, insertSpaces } = model.getOptions(); + const shiftIndent = (indentation: string, count?: number) => { + count = count || 1; + return ShiftCommand.shiftIndent(indentation, indentation.length + count, tabSize, indentSize, insertSpaces); + }; + const unshiftIndent = (indentation: string, count?: number) => { + count = count || 1; + return ShiftCommand.unshiftIndent(indentation, indentation.length + count, tabSize, indentSize, insertSpaces); + }; + const indentEdits: ISingleEditOperation[] = []; + + // indentation being passed to lines below + let globalIndent: string; + + // Calculate indentation for the first line + // If there is no passed-in indentation, we use the indentation of the first line as base. + const currentLineText = model.getLineContent(startLineNumber); + let adjustedLineContent = currentLineText; + if (inheritedIndent !== undefined && inheritedIndent !== null) { + globalIndent = inheritedIndent; + const oldIndentation = strings.getLeadingWhitespace(currentLineText); + + adjustedLineContent = globalIndent + currentLineText.substring(oldIndentation.length); + if (indentationRules.decreaseIndentPattern && indentationRules.decreaseIndentPattern.test(adjustedLineContent)) { + globalIndent = unshiftIndent(globalIndent); + adjustedLineContent = globalIndent + currentLineText.substring(oldIndentation.length); + + } + if (currentLineText !== adjustedLineContent) { + indentEdits.push(EditOperation.replaceMove(new Selection(startLineNumber, 1, startLineNumber, oldIndentation.length + 1), normalizeIndentation(globalIndent, indentSize, insertSpaces))); + } + } else { + globalIndent = strings.getLeadingWhitespace(currentLineText); + } + + // idealIndentForNextLine doesn't equal globalIndent when there is a line matching `indentNextLinePattern`. + let idealIndentForNextLine: string = globalIndent; + + if (indentationRules.increaseIndentPattern && indentationRules.increaseIndentPattern.test(adjustedLineContent)) { + idealIndentForNextLine = shiftIndent(idealIndentForNextLine); + globalIndent = shiftIndent(globalIndent); + } + else if (indentationRules.indentNextLinePattern && indentationRules.indentNextLinePattern.test(adjustedLineContent)) { + idealIndentForNextLine = shiftIndent(idealIndentForNextLine); + } + + startLineNumber++; + + // Calculate indentation adjustment for all following lines + for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) { + const text = model.getLineContent(lineNumber); + const oldIndentation = strings.getLeadingWhitespace(text); + const adjustedLineContent = idealIndentForNextLine + text.substring(oldIndentation.length); + + if (indentationRules.decreaseIndentPattern && indentationRules.decreaseIndentPattern.test(adjustedLineContent)) { + idealIndentForNextLine = unshiftIndent(idealIndentForNextLine); + globalIndent = unshiftIndent(globalIndent); + } + + if (oldIndentation !== idealIndentForNextLine) { + indentEdits.push(EditOperation.replaceMove(new Selection(lineNumber, 1, lineNumber, oldIndentation.length + 1), normalizeIndentation(idealIndentForNextLine, indentSize, insertSpaces))); + } + + // calculate idealIndentForNextLine + if (indentationRules.unIndentedLinePattern && indentationRules.unIndentedLinePattern.test(text)) { + // In reindent phase, if the line matches `unIndentedLinePattern` we inherit indentation from above lines + // but don't change globalIndent and idealIndentForNextLine. + continue; + } else if (indentationRules.increaseIndentPattern && indentationRules.increaseIndentPattern.test(adjustedLineContent)) { + globalIndent = shiftIndent(globalIndent); + idealIndentForNextLine = globalIndent; + } else if (indentationRules.indentNextLinePattern && indentationRules.indentNextLinePattern.test(adjustedLineContent)) { + idealIndentForNextLine = shiftIndent(idealIndentForNextLine); + } else { + idealIndentForNextLine = globalIndent; + } + } + + return indentEdits; +} diff --git a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts index 516966c9477..6ef21225b56 100644 --- a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts +++ b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts @@ -6,19 +6,25 @@ import * as assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { createTextModel } from 'vs/editor/test/common/testTextModel'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; -import { MetadataConsts, StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; -import { EncodedTokenizationResult, IState, TokenizationRegistry } from 'vs/editor/common/languages'; +import { MetadataConsts } from 'vs/editor/common/encodedTokenAttributes'; +import { EncodedTokenizationResult, IState, ITokenizationSupport, TokenizationRegistry } from 'vs/editor/common/languages'; import { ILanguageService } from 'vs/editor/common/languages/language'; -import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { NullState } from 'vs/editor/common/languages/nullTokenize'; import { AutoIndentOnPaste, IndentationToSpacesCommand, IndentationToTabsCommand } from 'vs/editor/contrib/indentation/browser/indentation'; import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { testCommand } from 'vs/editor/test/browser/testCommand'; import { javascriptIndentationRules } from 'vs/editor/test/common/modes/supports/javascriptIndentationRules'; import { javascriptOnEnterRules } from 'vs/editor/test/common/modes/supports/javascriptOnEnterRules'; -import { createTextModel } from 'vs/editor/test/common/testTextModel'; + +enum Language { + TypeScript, + Ruby +} function testIndentationToSpacesCommand(lines: string[], selection: Selection, tabSize: number, expectedLines: string[], expectedSelection: Selection): void { testCommand(lines, null, selection, (accessor, sel) => new IndentationToSpacesCommand(sel, tabSize), expectedLines, expectedSelection); @@ -28,7 +34,72 @@ function testIndentationToTabsCommand(lines: string[], selection: Selection, tab testCommand(lines, null, selection, (accessor, sel) => new IndentationToTabsCommand(sel, tabSize), expectedLines, expectedSelection); } -suite('Editor Contrib - Indentation to Spaces', () => { +function registerLanguage(instantiationService: TestInstantiationService, languageId: string, language: Language, disposables: DisposableStore) { + const languageService = instantiationService.get(ILanguageService); + registerLanguageConfiguration(instantiationService, languageId, language, disposables); + disposables.add(languageService.registerLanguage({ id: languageId })); +} + +// TODO@aiday-mar read directly the configuration file +function registerLanguageConfiguration(instantiationService: TestInstantiationService, languageId: string, language: Language, disposables: DisposableStore) { + const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); + switch (language) { + case Language.TypeScript: + disposables.add(languageConfigurationService.register(languageId, { + brackets: [ + ['{', '}'], + ['[', ']'], + ['(', ')'] + ], + comments: { + lineComment: '//', + blockComment: ['/*', '*/'] + }, + indentationRules: javascriptIndentationRules, + onEnterRules: javascriptOnEnterRules + })); + break; + case Language.Ruby: + disposables.add(languageConfigurationService.register(languageId, { + brackets: [ + ['{', '}'], + ['[', ']'], + ['(', ')'] + ], + indentationRules: { + decreaseIndentPattern: /^\s*([}\]]([,)]?\s*(#|$)|\.[a-zA-Z_]\w*\b)|(end|rescue|ensure|else|elsif)\b|(in|when)\s)/, + increaseIndentPattern: /^\s*((begin|class|(private|protected)\s+def|def|else|elsif|ensure|for|if|module|rescue|unless|until|when|in|while|case)|([^#]*\sdo\b)|([^#]*=\s*(case|if|unless)))\b([^#\{;]|(\"|'|\/).*\4)*(#.*)?$/, + }, + })); + break; + } +} + +function registerTokens(instantiationService: TestInstantiationService, tokens: { startIndex: number; value: number }[][], languageId: string, disposables: DisposableStore) { + let lineIndex = 0; + const languageService = instantiationService.get(ILanguageService); + const tokenizationSupport: ITokenizationSupport = { + getInitialState: () => NullState, + tokenize: undefined!, + tokenizeEncoded: (line: string, hasEOL: boolean, state: IState): EncodedTokenizationResult => { + const tokensOnLine = tokens[lineIndex++]; + const encodedLanguageId = languageService.languageIdCodec.encodeLanguageId(languageId); + const result = new Uint32Array(2 * tokensOnLine.length); + for (let i = 0; i < tokensOnLine.length; i++) { + result[2 * i] = tokensOnLine[i].startIndex; + result[2 * i + 1] = + ( + (encodedLanguageId << MetadataConsts.LANGUAGEID_OFFSET) + | (tokensOnLine[i].value << MetadataConsts.TOKEN_TYPE_OFFSET) + ); + } + return new EncodedTokenizationResult(result, state); + } + }; + disposables.add(TokenizationRegistry.register(languageId, tokenizationSupport)); +} + +suite('Change Indentation to Spaces - TypeScript/Javascript', () => { ensureNoDisposablesAreLeakedInTestSuite(); @@ -117,7 +188,7 @@ suite('Editor Contrib - Indentation to Spaces', () => { }); }); -suite('Editor Contrib - Indentation to Tabs', () => { +suite('Change Indentation to Tabs - TypeScript/Javascript', () => { ensureNoDisposablesAreLeakedInTestSuite(); @@ -202,7 +273,9 @@ suite('Editor Contrib - Indentation to Tabs', () => { }); }); -suite('Editor Contrib - Auto Indent On Paste', () => { +suite('`Full` Auto Indent On Paste - TypeScript/JavaScript', () => { + + const languageId = 'ts-test'; let disposables: DisposableStore; setup(() => { @@ -216,95 +289,61 @@ suite('Editor Contrib - Auto Indent On Paste', () => { ensureNoDisposablesAreLeakedInTestSuite(); test('issue #119225: Do not add extra leading space when pasting JSDoc', () => { - const languageId = 'leadingSpacePaste'; + const model = createTextModel("", languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { - const languageService = instantiationService.get(ILanguageService); - const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); - disposables.add(languageService.registerLanguage({ id: languageId })); - disposables.add(TokenizationRegistry.register(languageId, { - getInitialState: (): IState => NullState, - tokenize: () => { - throw new Error('not implemented'); - }, - tokenizeEncoded: (line: string, hasEOL: boolean, state: IState): EncodedTokenizationResult => { - const tokensArr: number[] = []; - if (line.indexOf('*') !== -1) { - tokensArr.push(0); - tokensArr.push(StandardTokenType.Comment << MetadataConsts.TOKEN_TYPE_OFFSET); - } else { - tokensArr.push(0); - tokensArr.push(StandardTokenType.Other << MetadataConsts.TOKEN_TYPE_OFFSET); - } - const tokens = new Uint32Array(tokensArr.length); - for (let i = 0; i < tokens.length; i++) { - tokens[i] = tokensArr[i]; - } - return new EncodedTokenizationResult(tokens, state); - } - })); - disposables.add(languageConfigurationService.register(languageId, { - brackets: [ - ['{', '}'], - ['[', ']'], - ['(', ')'] - ], - comments: { - lineComment: '//', - blockComment: ['/*', '*/'] - }, - indentationRules: javascriptIndentationRules, - onEnterRules: javascriptOnEnterRules - })); - const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); + withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { const pasteText = [ '/**', ' * JSDoc', ' */', 'function a() {}' ].join('\n'); - + const tokens = [ + [ + { startIndex: 0, value: 1 }, + { startIndex: 3, value: 1 }, + ], + [ + { startIndex: 0, value: 1 }, + { startIndex: 2, value: 1 }, + { startIndex: 8, value: 1 }, + ], + [ + { startIndex: 0, value: 1 }, + { startIndex: 1, value: 1 }, + { startIndex: 3, value: 0 }, + ], + [ + { startIndex: 0, value: 0 }, + { startIndex: 8, value: 0 }, + { startIndex: 9, value: 0 }, + { startIndex: 10, value: 0 }, + { startIndex: 11, value: 0 }, + { startIndex: 12, value: 0 }, + { startIndex: 13, value: 0 }, + { startIndex: 14, value: 0 }, + { startIndex: 15, value: 0 }, + ] + ]; + registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + registerTokens(instantiationService, tokens, languageId, disposables); + const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); viewModel.paste(pasteText, true, undefined, 'keyboard'); autoIndentOnPasteController.trigger(new Range(1, 1, 4, 16)); assert.strictEqual(model.getValue(), pasteText); }); }); -}); - -suite('Editor Contrib - Keep Indent On Paste', () => { - let disposables: DisposableStore; - - setup(() => { - disposables = new DisposableStore(); - }); - - teardown(() => { - disposables.dispose(); - }); - - ensureNoDisposablesAreLeakedInTestSuite(); test('issue #167299: Blank line removes indent', () => { - const languageId = 'blankLineRemovesIndent'; + const model = createTextModel("", languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { - const languageService = instantiationService.get(ILanguageService); - const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); - disposables.add(languageService.registerLanguage({ id: languageId })); - disposables.add(languageConfigurationService.register(languageId, { - brackets: [ - ['{', '}'], - ['[', ']'], - ['(', ')'] - ], - indentationRules: javascriptIndentationRules, - onEnterRules: javascriptOnEnterRules - })); - const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); + withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { + + // no need for tokenization because there are no comments const pasteText = [ '', 'export type IncludeReference =', @@ -319,14 +358,393 @@ suite('Editor Contrib - Keep Indent On Paste', () => { '}' ].join('\n'); + registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); viewModel.paste(pasteText, true, undefined, 'keyboard'); autoIndentOnPasteController.trigger(new Range(1, 1, 11, 2)); assert.strictEqual(model.getValue(), pasteText); }); }); + + // Failing tests found in issues... + + test.skip('issue #181065: Incorrect paste of object within comment', () => { + + // https://github.com/microsoft/vscode/issues/181065 + + const model = createTextModel("", languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { + const text = [ + '/**', + ' * @typedef {', + ' * }', + ' */' + ].join('\n'); + const tokens = [ + [ + { startIndex: 0, value: 1 }, + { startIndex: 3, value: 1 }, + ], + [ + { startIndex: 0, value: 1 }, + { startIndex: 2, value: 1 }, + { startIndex: 3, value: 1 }, + { startIndex: 11, value: 1 }, + { startIndex: 12, value: 0 }, + { startIndex: 13, value: 0 }, + ], + [ + { startIndex: 0, value: 1 }, + { startIndex: 2, value: 0 }, + { startIndex: 3, value: 0 }, + { startIndex: 4, value: 0 }, + ], + [ + { startIndex: 0, value: 1 }, + { startIndex: 1, value: 1 }, + { startIndex: 3, value: 0 }, + ] + ]; + registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + registerTokens(instantiationService, tokens, languageId, disposables); + const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); + viewModel.paste(text, true, undefined, 'keyboard'); + autoIndentOnPasteController.trigger(new Range(1, 1, 4, 4)); + assert.strictEqual(model.getValue(), text); + }); + }); + + test.skip('issue #86301: preserve cursor at inserted indentation level', () => { + + // https://github.com/microsoft/vscode/issues/86301 + + const model = createTextModel([ + '() => {', + '', + '}', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { + editor.setSelection(new Selection(2, 1, 2, 1)); + const text = [ + '() => {', + '', + '}', + '' + ].join('\n'); + registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); + viewModel.paste(text, true, undefined, 'keyboard'); + autoIndentOnPasteController.trigger(new Range(2, 1, 5, 1)); + + // notes: + // why is line 3 not indented to the same level as line 2? + // looks like the indentation is inserted correctly at line 5, but the cursor does not appear at the maximum indentation level? + assert.strictEqual(model.getValue(), [ + '() => {', + ' () => {', + ' ', // <- should also be indented + ' }', + ' ', // <- cursor should be at the end of the indentation + '}', + ].join('\n')); + + const selection = viewModel.getSelection(); + assert.deepStrictEqual(selection, new Selection(5, 5, 5, 5)); + }); + }); + + test.skip('issue #85781: indent line with extra white space', () => { + + // https://github.com/microsoft/vscode/issues/85781 + // note: still to determine whether this is a bug or not + + const model = createTextModel([ + '() => {', + ' console.log("a");', + '}', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { + editor.setSelection(new Selection(2, 5, 2, 5)); + const text = [ + '() => {', + ' console.log("b")', + '}', + ' ' + ].join('\n'); + registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); + viewModel.paste(text, true, undefined, 'keyboard'); + // todo@aiday-mar, make sure range is correct, and make test work as in real life + autoIndentOnPasteController.trigger(new Range(2, 5, 5, 6)); + assert.strictEqual(model.getValue(), [ + '() => {', + ' () => {', + ' console.log("b")', + ' }', + ' console.log("a");', + '}', + ].join('\n')); + }); + }); + + test.skip('issue #29589: incorrect indentation of closing brace on paste', () => { + + // https://github.com/microsoft/vscode/issues/29589 + + const model = createTextModel('', languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { + editor.setSelection(new Selection(2, 5, 2, 5)); + const text = [ + 'function makeSub(a,b) {', + 'subsent = sent.substring(a,b);', + 'return subsent;', + '}', + ].join('\n'); + registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); + viewModel.paste(text, true, undefined, 'keyboard'); + // todo@aiday-mar, make sure range is correct, and make test work as in real life + autoIndentOnPasteController.trigger(new Range(1, 1, 4, 2)); + assert.strictEqual(model.getValue(), [ + 'function makeSub(a,b) {', + ' subsent = sent.substring(a,b);', + ' return subsent;', + '}', + ].join('\n')); + }); + }); }); -suite('Editor Contrib - Auto Dedent On Type', () => { +suite('`Full` Auto Indent On Type - TypeScript/JavaScript', () => { + + const languageId = "ts-test"; + let disposables: DisposableStore; + + setup(() => { + disposables = new DisposableStore(); + }); + + teardown(() => { + disposables.dispose(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + // Test added so there is at least one non-ignored test in this suite + test('temporary test', () => { + assert.ok(true); + }); + + // Failing tests from issues... + + test.skip('issue #208215: indent after arrow function', () => { + + // https://github.com/microsoft/vscode/issues/208215 + // consider the regex: /^\s*(var|const|let)\s+\w+\s*=\s*\(.*\)\s*=>\s*$/ + + const model = createTextModel("", languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { + + registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + + viewModel.type('const add1 = (n) =>'); + viewModel.type("\n", 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'const add1 = (n) =>', + ' ', + ].join('\n')); + }); + }); + + test.skip('issue #208215: outdented after semicolon detected after arrow function', () => { + + // Notes: we want to outdent after having detected a semi-colon which marks the end of the line, but only when we have detected an arrow function + // We could use one outdent pattern corresponding per indent pattern, and not a generic outdent and indent pattern + + const model = createTextModel([ + 'const add1 = (n) =>', + ' console.log("hi");', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { + + registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + + editor.setSelection(new Selection(2, 24, 2, 24)); + viewModel.type("\n", 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'const add1 = (n) =>', + ' console.log("hi");', + '', + ].join('\n')); + }); + }); + + test.skip('issue #116843: indent after arrow function', () => { + + // https://github.com/microsoft/vscode/issues/116843 + + const model = createTextModel("", languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { + + registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + + viewModel.type([ + 'const add1 = (n) =>', + ' n + 1;', + ].join('\n')); + viewModel.type("\n", 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'const add1 = (n) =>', + ' n + 1;', + '', + ].join('\n')); + }); + }); + + test.skip('issue #40115: keep indentation when added', () => { + + // https://github.com/microsoft/vscode/issues/40115 + + const model = createTextModel('function foo() {}', languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { + + registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + + editor.setSelection(new Selection(1, 17, 1, 17)); + viewModel.type("\n", 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'function foo() {', + ' ', + '}', + ].join('\n')); + editor.setSelection(new Selection(2, 5, 2, 5)); + viewModel.type("\n", 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'function foo() {', + ' ', + ' ', + '}', + ].join('\n')); + }); + }); + + test.skip('issue #193875: incorrect indentation on enter', () => { + + // https://github.com/microsoft/vscode/issues/193875 + + const model = createTextModel([ + '{', + ' for(;;)', + ' for(;;) {}', + '}', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { + + registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + editor.setSelection(new Selection(3, 14, 3, 14)); + viewModel.type("\n", 'keyboard'); + assert.strictEqual(model.getValue(), [ + '{', + ' for(;;)', + ' for(;;) {', + ' ', + ' }', + '}', + ].join('\n')); + }); + }); + + test.skip('issue #43244: incorrect indentation', () => { + + // https://github.com/microsoft/vscode/issues/43244 + // potential regex to fix: "^.*[if|while|for]\s*\(.*\)\s*", + + const model = createTextModel([ + 'function f() {', + ' if (condition)', + ' return;', + '}' + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { + + registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + editor.setSelection(new Selection(3, 16, 3, 16)); + viewModel.type("\n", 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'function f() {', + ' if (condition)', + ' return;', + '', + '}', + ].join('\n')); + + viewModel.type("\n", 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'function f() {', + ' if (condition)', + ' return;', + '', + '', + '}', + ].join('\n')); + }); + }); + + test.skip('issue #208232: incorrect indentation inside of comments', () => { + + // https://github.com/microsoft/vscode/issues/208232 + + const model = createTextModel([ + '/**', + 'indentation done for {', + '*/' + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { + + registerLanguage(instantiationService, languageId, Language.TypeScript, disposables); + editor.setSelection(new Selection(2, 23, 2, 23)); + viewModel.type("\n", 'keyboard'); + assert.strictEqual(model.getValue(), [ + '/**', + 'indentation done for {', + '', + '*/' + ].join('\n')); + }); + }); + + // Add tests for: + // https://github.com/microsoft/vscode/issues/88638 + // https://github.com/microsoft/vscode/issues/63388 + // https://github.com/microsoft/vscode/issues/46401 + // https://github.com/microsoft/vscode/issues/174044 +}); + +suite('Auto Indent On Type - Ruby', () => { + + const languageId = "ruby-test"; let disposables: DisposableStore; setup(() => { @@ -340,45 +758,14 @@ suite('Editor Contrib - Auto Dedent On Type', () => { ensureNoDisposablesAreLeakedInTestSuite(); test('issue #198350: in or when incorrectly match non keywords for Ruby', () => { - const languageId = "ruby"; + const model = createTextModel("", languageId, {}); disposables.add(model); withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - const languageService = instantiationService.get(ILanguageService); - const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); - disposables.add(languageService.registerLanguage({ id: languageId })); - const languageModel = languageConfigurationService.register(languageId, { - brackets: [ - ['{', '}'], - ['[', ']'], - ['(', ')'] - ], - indentationRules: { - decreaseIndentPattern: /\s*([}\]]([,)]?\s*(#|$)|\.[a-zA-Z_]\w*\b)|(end|rescue|ensure|else|elsif|when|in)\b)/, - increaseIndentPattern: /^\s*((begin|class|(private|protected)\s+def|def|else|elsif|ensure|for|if|module|rescue|unless|until|when|in|while|case)|([^#]*\sdo\b)|([^#]*=\s*(case|if|unless)))\b([^#\{;]|(\"|'|\/).*\4)*(#.*)?$/, - }, - }); - viewModel.type("def foo\n i", 'keyboard'); - viewModel.type("n", 'keyboard'); - // The 'in' triggers decreaseIndentPattern immediately, which is incorrect - assert.strictEqual(model.getValue(), "def foo\nin"); - languageModel.dispose(); + registerLanguage(instantiationService, languageId, Language.Ruby, disposables); - const improvedLanguageModel = languageConfigurationService.register(languageId, { - brackets: [ - ['{', '}'], - ['[', ']'], - ['(', ')'] - ], - indentationRules: { - decreaseIndentPattern: /^\s*([}\]]([,)]?\s*(#|$)|\.[a-zA-Z_]\w*\b)|(end|rescue|ensure|else|elsif)\b|(in|when)\s)/, - increaseIndentPattern: /^\s*((begin|class|(private|protected)\s+def|def|else|elsif|ensure|for|if|module|rescue|unless|until|when|in|while|case)|([^#]*\sdo\b)|([^#]*=\s*(case|if|unless)))\b([^#\{;]|(\"|'|\/).*\4)*(#.*)?$/, - }, - }); - - viewModel.model.setValue(""); viewModel.type("def foo\n i"); viewModel.type("n", 'keyboard'); assert.strictEqual(model.getValue(), "def foo\n in"); @@ -390,7 +777,6 @@ suite('Editor Contrib - Auto Dedent On Type', () => { assert.strictEqual(model.getValue(), " # in"); viewModel.type(" ", 'keyboard'); assert.strictEqual(model.getValue(), " # in "); - improvedLanguageModel.dispose(); }); }); }); diff --git a/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts b/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts index 8db880a15ad..68614a2f432 100644 --- a/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts +++ b/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts @@ -13,7 +13,7 @@ import { ITextModel } from 'vs/editor/common/model'; import { CompleteEnterAction, IndentAction } from 'vs/editor/common/languages/languageConfiguration'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { IndentConsts } from 'vs/editor/common/languages/supports/indentRules'; -import * as indentUtils from 'vs/editor/contrib/indentation/browser/indentUtils'; +import * as indentUtils from 'vs/editor/contrib/indentation/common/indentUtils'; import { getGoodIndentForLine, getIndentMetadata, IIndentConverter, IVirtualModel } from 'vs/editor/common/languages/autoIndent'; import { getEnterAction } from 'vs/editor/common/languages/enterAction'; diff --git a/src/vs/editor/contrib/links/browser/links.ts b/src/vs/editor/contrib/links/browser/links.ts index 9527453bf32..073c85c85ea 100644 --- a/src/vs/editor/contrib/links/browser/links.ts +++ b/src/vs/editor/contrib/links/browser/links.ts @@ -237,9 +237,9 @@ export class LinkDetector extends Disposable implements IEditorContribution { const fsPath = resources.originalFSPath(parsedUri); let relativePath: string | null = null; - if (fsPath.startsWith('/./')) { + if (fsPath.startsWith('/./') || fsPath.startsWith('\\.\\')) { relativePath = `.${fsPath.substr(1)}`; - } else if (fsPath.startsWith('//./')) { + } else if (fsPath.startsWith('//./') || fsPath.startsWith('\\\\.\\')) { relativePath = `.${fsPath.substr(2)}`; } diff --git a/src/vs/editor/contrib/rename/browser/rename.ts b/src/vs/editor/contrib/rename/browser/rename.ts index 8daef7eff27..dba920953ba 100644 --- a/src/vs/editor/contrib/rename/browser/rename.ts +++ b/src/vs/editor/contrib/rename/browser/rename.ts @@ -38,7 +38,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { IEditorProgressService } from 'vs/platform/progress/common/progress'; import { Registry } from 'vs/platform/registry/common/platform'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { CONTEXT_RENAME_INPUT_VISIBLE, NewNameSource, RenameInputField, RenameInputFieldResult } from './renameInputField'; +import { CONTEXT_RENAME_INPUT_VISIBLE, NewNameSource, RenameInputFieldResult, RenameWidget } from './renameInputField'; class RenameSkeleton { @@ -138,7 +138,7 @@ class RenameController implements IEditorContribution { return editor.getContribution(RenameController.ID); } - private readonly _renameInputField: RenameInputField; + private readonly _renameInputField: RenameWidget; private readonly _disposableStore = new DisposableStore(); private _cts: CancellationTokenSource = new CancellationTokenSource(); @@ -153,7 +153,7 @@ class RenameController implements IEditorContribution { @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, @ITelemetryService private readonly _telemetryService: ITelemetryService, ) { - this._renameInputField = this._disposableStore.add(this._instaService.createInstance(RenameInputField, this.editor, ['acceptRenameInput', 'acceptRenameInputWithPreview'])); + this._renameInputField = this._disposableStore.add(this._instaService.createInstance(RenameWidget, this.editor, ['acceptRenameInput', 'acceptRenameInputWithPreview'])); } dispose(): void { @@ -229,23 +229,19 @@ class RenameController implements IEditorContribution { const model = this.editor.getModel(); // @ulugbekna: assumes editor still has a model, otherwise, cts1 should've been cancelled - const renameCandidatesCts = new CancellationTokenSource(cts2.token); const newSymbolNamesProviders = this._languageFeaturesService.newSymbolNamesProvider.all(model); - const newSymbolNameProvidersResults = newSymbolNamesProviders.map(p => p.provideNewSymbolNames(model, loc.range, renameCandidatesCts.token)); - trace(`requested new symbol names from ${newSymbolNamesProviders.length} providers`); - const selection = this.editor.getSelection(); - let selectionStart = 0; - let selectionEnd = loc.text.length; - - if (!Range.isEmpty(selection) && !Range.spansMultipleLines(selection) && Range.containsRange(loc.range, selection)) { - selectionStart = Math.max(0, selection.startColumn - loc.range.startColumn); - selectionEnd = Math.min(loc.range.endColumn, selection.endColumn) - loc.range.startColumn; - } + const requestRenameSuggestions = (cts: CancellationToken) => newSymbolNamesProviders.map(p => p.provideNewSymbolNames(model, loc.range, cts)); trace('creating rename input field and awaiting its result'); const supportPreview = this._bulkEditService.hasPreviewHandler() && this._configService.getValue(this.editor.getModel().uri, 'editor.rename.enablePreview'); - const inputFieldResult = await this._renameInputField.getInput(loc.range, loc.text, selectionStart, selectionEnd, supportPreview, newSymbolNameProvidersResults, renameCandidatesCts); + const inputFieldResult = await this._renameInputField.getInput( + loc.range, + loc.text, + supportPreview, + requestRenameSuggestions, + cts2 + ); trace('received response from rename input field'); if (newSymbolNamesProviders.length > 0) { // @ulugbekna: we're interested only in telemetry for rename suggestions currently diff --git a/src/vs/editor/contrib/rename/browser/renameInputField.ts b/src/vs/editor/contrib/rename/browser/renameInputField.ts index af5f430254b..eb6bfde534b 100644 --- a/src/vs/editor/contrib/rename/browser/renameInputField.ts +++ b/src/vs/editor/contrib/rename/browser/renameInputField.ts @@ -23,7 +23,7 @@ import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { FontInfo } from 'vs/editor/common/config/fontInfo'; import { IDimension } from 'vs/editor/common/core/dimension'; import { Position } from 'vs/editor/common/core/position'; -import { IRange } from 'vs/editor/common/core/range'; +import { IRange, Range } from 'vs/editor/common/core/range'; import { ScrollType } from 'vs/editor/common/editorCommon'; import { NewSymbolName, NewSymbolNameTag, ProviderResult } from 'vs/editor/common/languages'; import { localize } from 'vs/nls'; @@ -85,7 +85,13 @@ interface IRenameInputField { /** * @returns a `boolean` standing for `shouldFocusEditor`, if user didn't pick a new name, or a {@link RenameInputFieldResult} */ - getInput(where: IRange, value: string, selectionStart: number, selectionEnd: number, supportPreview: boolean, candidates: ProviderResult[], cts: CancellationTokenSource): Promise; + getInput( + where: IRange, + currentName: string, + supportPreview: boolean, + requestRenameSuggestions: (cts: CancellationToken) => ProviderResult[], + cts: CancellationTokenSource + ): Promise; acceptInput(wantsPreview: boolean): void; cancelInput(focusEditor: boolean, caller: string): void; @@ -94,7 +100,7 @@ interface IRenameInputField { focusPreviousRenameSuggestion(): void; } -export class RenameInputField implements IRenameInputField, IContentWidget, IDisposable { +export class RenameWidget implements IRenameInputField, IContentWidget, IDisposable { // implement IContentWidget readonly allowEditorOverflow: boolean = true; @@ -127,6 +133,8 @@ export class RenameInputField implements IRenameInputField, IContentWidget, IDis */ private _timeBeforeFirstInputFieldEdit: number | undefined; + private _renameCandidateProvidersCts: CancellationTokenSource | undefined; + private readonly _visibleContextKey: IContextKey; private readonly _disposables = new DisposableStore(); @@ -194,6 +202,9 @@ export class RenameInputField implements IRenameInputField, IContentWidget, IDis this._isEditingRenameCandidate = true; } this._timeBeforeFirstInputFieldEdit ??= this._beforeFirstInputFieldEditSW.elapsed(); + if (this._renameCandidateProvidersCts?.token.isCancellationRequested === false) { + this._renameCandidateProvidersCts.cancel(); + } this._renameCandidateListView?.clearFocus(); }) ); @@ -346,7 +357,19 @@ export class RenameInputField implements IRenameInputField, IContentWidget, IDis } } - getInput(where: IRange, currentName: string, selectionStart: number, selectionEnd: number, supportPreview: boolean, candidates: ProviderResult[], cts: CancellationTokenSource): Promise { + getInput( + where: IRange, + currentName: string, + supportPreview: boolean, + requestRenameSuggestions: (cts: CancellationToken) => ProviderResult[], + cts: CancellationTokenSource + ): Promise { + + const { start: selectionStart, end: selectionEnd } = this._getSelection(where, currentName); + + this._renameCandidateProvidersCts = new CancellationTokenSource(); + const candidates = requestRenameSuggestions(this._renameCandidateProvidersCts.token); + this._updateRenameCandidates(candidates, currentName, cts.token); this._isEditingRenameCandidate = false; @@ -365,8 +388,12 @@ export class RenameInputField implements IRenameInputField, IContentWidget, IDis const disposeOnDone = new DisposableStore(); disposeOnDone.add(toDisposable(() => cts.dispose(true))); // @ulugbekna: this may result in `this.cancelInput` being called twice, but it should be safe since we set it to undefined after 1st call - - this._updateRenameCandidates(candidates, currentName, cts.token); + disposeOnDone.add(toDisposable(() => { + if (this._renameCandidateProvidersCts !== undefined) { + this._renameCandidateProvidersCts.dispose(true); + this._renameCandidateProvidersCts = undefined; + } + })); const inputResult = new DeferredPromise(); @@ -433,6 +460,24 @@ export class RenameInputField implements IRenameInputField, IContentWidget, IDis return inputResult.p; } + /** + * This allows selecting only part of the symbol name in the input field based on the selection in the editor + */ + private _getSelection(where: IRange, currentName: string): { start: number; end: number } { + assertType(this._editor.hasModel()); + + const selection = this._editor.getSelection(); + let start = 0; + let end = currentName.length; + + if (!Range.isEmpty(selection) && !Range.spansMultipleLines(selection) && Range.containsRange(where, selection)) { + start = Math.max(0, selection.startColumn - where.startColumn); + end = Math.min(where.endColumn, selection.endColumn) - where.startColumn; + } + + return { start, end }; + } + private _show(): void { this._trace('invoking _show'); this._editor.revealLineInCenterIfOutsideViewport(this._position!.lineNumber, ScrollType.Smooth); @@ -508,8 +553,8 @@ export class RenameInputField implements IRenameInputField, IContentWidget, IDis return this._editor.getTopForLineNumber(this._position!.lineNumber) - this._editor.getTopForLineNumber(firstLineInViewport); } - private _trace(...args: any[]) { - this._logService.trace('RenameInputField', ...args); + private _trace(...args: unknown[]) { + this._logService.trace('RenameWidget', ...args); } } diff --git a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts index 05305694e57..c80bdf63686 100644 --- a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts @@ -55,7 +55,7 @@ export interface IActionDescriptor { */ label: string; /** - * Precondition rule. + * Precondition rule. The value should be a [context key expression](https://code.visualstudio.com/docs/getstarted/keybindings#_when-clause-contexts). */ precondition?: string; /** diff --git a/src/vs/editor/test/common/modes/supports/javascriptOnEnterRules.ts b/src/vs/editor/test/common/modes/supports/javascriptOnEnterRules.ts index 5c8f580f228..12c05394dd9 100644 --- a/src/vs/editor/test/common/modes/supports/javascriptOnEnterRules.ts +++ b/src/vs/editor/test/common/modes/supports/javascriptOnEnterRules.ts @@ -29,5 +29,15 @@ export const javascriptOnEnterRules = [ // e.g. *-----*/| beforeText: /^(\t|[ ])*[ ]\*[^/]*\*\/\s*$/, action: { indentAction: IndentAction.None, removeText: 1 } - } + }, + { + beforeText: /^\s*(\bcase\s.+:|\bdefault:)$/, + afterText: /^(?!\s*(\bcase\b|\bdefault\b))/, + action: { indentAction: IndentAction.Indent } + }, + { + previousLineText: /^\s*(((else ?)?if|for|while)\s*\(.*\)\s*|else\s*)$/, + beforeText: /^\s+([^{i\s]|i(?!f\b))/, + action: { indentAction: IndentAction.Outdent } + }, ]; diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 509f549d41b..ae56ff86935 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -1251,7 +1251,7 @@ declare namespace monaco.editor { */ label: string; /** - * Precondition rule. + * Precondition rule. The value should be a [context key expression](https://code.visualstudio.com/docs/getstarted/keybindings#_when-clause-contexts). */ precondition?: string; /** diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts index cc157af7ab3..18d4fcd795a 100644 --- a/src/vs/platform/environment/common/argv.ts +++ b/src/vs/platform/environment/common/argv.ts @@ -140,4 +140,5 @@ export interface NativeParsedArgs { 'log-net-log'?: string; 'vmodule'?: string; 'disable-dev-shm-usage'?: boolean; + 'ozone-platform'?: string; } diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 0d3a9795ffb..327c18c18ad 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -204,6 +204,7 @@ export const OPTIONS: OptionDescriptions> = { '_urls': { type: 'string[]' }, 'disable-dev-shm-usage': { type: 'boolean' }, 'profile-temp': { type: 'boolean' }, + 'ozone-platform': { type: 'string' }, _: { type: 'string[]' } // main arguments }; diff --git a/src/vs/platform/quickinput/browser/media/quickInput.css b/src/vs/platform/quickinput/browser/media/quickInput.css index f8e7aba2b91..3afa06e5888 100644 --- a/src/vs/platform/quickinput/browser/media/quickInput.css +++ b/src/vs/platform/quickinput/browser/media/quickInput.css @@ -59,7 +59,7 @@ .quick-input-header { display: flex; - padding: 8px 6px 6px 6px; + padding: 8px 6px 2px 6px; } .quick-input-widget.hidden-input .quick-input-header { @@ -263,11 +263,16 @@ overflow: hidden; } -.quick-input-list .monaco-highlighted-label .highlight { +/* preserve list-like styling instead of tree-like styling */ +.quick-input-list .monaco-list .monaco-list-row .monaco-highlighted-label .highlight { font-weight: bold; - /* preserve list-like styling instead of tree-like styling */ + background-color: unset; + color: var(--vscode-list-highlightForeground) !important; +} + +/* preserve list-like styling instead of tree-like styling */ +.quick-input-list .monaco-list .monaco-list-row.focused .monaco-highlighted-label .highlight { color: var(--vscode-list-focusHighlightForeground) !important; - background-color: transparent; } .quick-input-list .quick-input-list-entry .quick-input-list-separator { @@ -318,12 +323,21 @@ background: none; } -/* Quick input separators as full-row item */ .quick-input-list .quick-input-list-separator-as-item { - font-weight: 600; + padding: 4px 6px; font-size: 12px; } +/* Quick input separators as full-row item */ +.quick-input-list .quick-input-list-separator-as-item .label-name { + font-weight: 600; +} + +.quick-input-list .quick-input-list-separator-as-item .label-description { + /* Override default description opacity so we don't have a contrast ratio issue. */ + opacity: 1 !important; +} + /* Hide border when the item becomes the sticky one */ .quick-input-list .monaco-tree-sticky-row .quick-input-list-entry.quick-input-list-separator-as-item.quick-input-list-separator-border { border-top-style: none; diff --git a/src/vs/platform/quickinput/browser/quickAccess.ts b/src/vs/platform/quickinput/browser/quickAccess.ts index cb35e451aa1..88169c32ed5 100644 --- a/src/vs/platform/quickinput/browser/quickAccess.ts +++ b/src/vs/platform/quickinput/browser/quickAccess.ts @@ -92,6 +92,10 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon } } + // Store the existing selection if there was one. + const visibleSelection = visibleQuickAccess?.picker?.valueSelection; + const visibleValue = visibleQuickAccess?.picker?.value; + // Create a picker for the provider to use with the initial value // and adjust the filtering to exclude the prefix from filtering const disposables = new DisposableStore(); @@ -148,6 +152,11 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon // on the onDidHide event. picker.show(); + // If the previous picker had a selection and the value is unchanged, we should set that in the new picker. + if (visibleSelection && visibleValue === value) { + picker.valueSelection = visibleSelection; + } + // Pick mode: return with promise if (pick) { return pickPromise?.p; diff --git a/src/vs/platform/quickinput/browser/quickInput.ts b/src/vs/platform/quickinput/browser/quickInput.ts index 3618afe711b..0cce0ef4228 100644 --- a/src/vs/platform/quickinput/browser/quickInput.ts +++ b/src/vs/platform/quickinput/browser/quickInput.ts @@ -723,7 +723,15 @@ export class QuickPick extends QuickInput implements I return this.ui.keyMods; } - set valueSelection(valueSelection: Readonly<[number, number]>) { + get valueSelection() { + const selection = this.ui.inputBox.getSelection(); + if (!selection) { + return undefined; + } + return [selection.start, selection.end]; + } + + set valueSelection(valueSelection: Readonly<[number, number]> | undefined) { this._valueSelection = valueSelection; this.valueSelectionUpdated = true; this.update(); @@ -1167,7 +1175,15 @@ export class InputBox extends QuickInput implements IInputBox { this.update(); } - set valueSelection(valueSelection: Readonly<[number, number]>) { + get valueSelection() { + const selection = this.ui.inputBox.getSelection(); + if (!selection) { + return undefined; + } + return [selection.start, selection.end]; + } + + set valueSelection(valueSelection: Readonly<[number, number]> | undefined) { this._valueSelection = valueSelection; this.valueSelectionUpdated = true; this.update(); diff --git a/src/vs/platform/quickinput/browser/quickInputBox.ts b/src/vs/platform/quickinput/browser/quickInputBox.ts index b3c6694387c..9ee1440a91a 100644 --- a/src/vs/platform/quickinput/browser/quickInputBox.ts +++ b/src/vs/platform/quickinput/browser/quickInputBox.ts @@ -59,6 +59,10 @@ export class QuickInputBox extends Disposable { this.findInput.inputBox.select(range); } + getSelection(): IRange | null { + return this.findInput.inputBox.getSelection(); + } + isSelectionAtEnd(): boolean { return this.findInput.inputBox.isSelectionAtEnd(); } diff --git a/src/vs/platform/quickinput/browser/quickInputTree.ts b/src/vs/platform/quickinput/browser/quickInputTree.ts index 0b64f90f402..aa4329a2867 100644 --- a/src/vs/platform/quickinput/browser/quickInputTree.ts +++ b/src/vs/platform/quickinput/browser/quickInputTree.ts @@ -110,6 +110,8 @@ class BaseQuickPickItemElement implements IQuickPickElement { saneAriaLabel }; }); + this._saneDescription = mainItem.description; + this._saneTooltip = mainItem.tooltip; } // #region Lazy Getters @@ -144,7 +146,7 @@ class BaseQuickPickItemElement implements IQuickPickElement { this._hidden = value; } - protected _saneDescription?: string; + private _saneDescription?: string; get saneDescription() { return this._saneDescription; } @@ -160,7 +162,7 @@ class BaseQuickPickItemElement implements IQuickPickElement { this._saneDetail = value; } - protected _saneTooltip?: string | IMarkdownString | HTMLElement; + private _saneTooltip?: string | IMarkdownString | HTMLElement; get saneTooltip() { return this._saneTooltip; } @@ -210,9 +212,7 @@ class QuickPickItemElement extends BaseQuickPickItemElement { ? Event.map(Event.filter<{ element: IQuickPickElement; checked: boolean }>(this._onChecked.event, e => e.element === this), e => e.checked) : Event.None; - this._saneDescription = item.description; this._saneDetail = item.detail; - this._saneTooltip = this.item.tooltip; this._labelHighlights = item.highlights?.label; this._descriptionHighlights = item.highlights?.description; this._detailHighlights = item.highlights?.detail; @@ -277,9 +277,8 @@ class QuickPickSeparatorElement extends BaseQuickPickItemElement { class QuickInputItemDelegate implements IListVirtualDelegate { getHeight(element: IQuickPickElement): number { - if (!element.item) { - // must be a separator - return 24; + if (element instanceof QuickPickSeparatorElement) { + return 30; } return element.saneDetail ? 44 : 22; } diff --git a/src/vs/platform/quickinput/common/quickInput.ts b/src/vs/platform/quickinput/common/quickInput.ts index 79224918075..820544bcd69 100644 --- a/src/vs/platform/quickinput/common/quickInput.ts +++ b/src/vs/platform/quickinput/common/quickInput.ts @@ -57,6 +57,7 @@ export interface IQuickPickSeparator { type: 'separator'; id?: string; label?: string; + description?: string; ariaLabel?: string; buttons?: readonly IQuickInputButton[]; tooltip?: string | IMarkdownString; diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index 42ff6482c51..6010284aec3 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -11,7 +11,7 @@ import { JSONValidationExtensionPoint } from 'vs/workbench/api/common/jsonValida import { ColorExtensionPoint } from 'vs/workbench/services/themes/common/colorExtensionPoint'; import { IconExtensionPoint } from 'vs/workbench/services/themes/common/iconExtensionPoint'; import { TokenClassificationExtensionPoints } from 'vs/workbench/services/themes/common/tokenClassificationExtensionPoint'; -import { LanguageConfigurationFileHandler } from 'vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint'; +import { LanguageConfigurationFileHandler } from 'vs/workbench/contrib/codeEditor/common/languageConfigurationExtensionPoint'; import { StatusBarItemsExtensionPoint } from 'vs/workbench/api/browser/statusBarExtensionPoint'; // --- mainThread participants diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index 631f51548dc..0aaaee8cf14 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -20,17 +20,17 @@ import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart'; import { AddDynamicVariableAction, IAddDynamicVariableContext } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables'; import { ChatAgentLocation, IChatAgentImplementation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; import { ChatRequestAgentPart } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; import { IChatFollowup, IChatProgress, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; -type AgentData = { +interface AgentData { dispose: () => void; - name: string; + id: string; + extensionId: ExtensionIdentifier; hasFollowups?: boolean; -}; +} @extHostNamedCustomer(MainContext.MainThreadChatAgents2) export class MainThreadChatAgents2 extends Disposable implements MainThreadChatAgentsShape2 { @@ -48,7 +48,6 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IChatContributionService private readonly _chatContributionService: IChatContributionService, ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostChatAgents2); @@ -59,7 +58,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA this._register(this._chatService.onDidPerformUserAction(e => { if (typeof e.agentId === 'string') { for (const [handle, agent] of this._agents) { - if (agent.name === e.agentId) { + if (agent.id === e.agentId) { if (e.action.kind === 'vote') { this._proxy.$acceptFeedback(handle, e.result ?? {}, e.action.direction); } else { @@ -76,10 +75,16 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA this._agents.deleteAndDispose(handle); } - $registerAgent(handle: number, extension: ExtensionIdentifier, name: string, metadata: IExtensionChatAgentMetadata, allowDynamic: boolean): void { - const staticAgentRegistration = this._chatContributionService.registeredParticipants.find(p => p.extensionId.value === extension.value && p.name === name); - if (!staticAgentRegistration && !allowDynamic) { - throw new Error(`chatParticipant must be declared in package.json: ${name}`); + $registerAgent(handle: number, extension: ExtensionIdentifier, id: string, metadata: IExtensionChatAgentMetadata, dynamicProps: { name: string; description: string } | undefined): void { + const staticAgentRegistration = this._chatAgentService.getAgent(id); + if (!staticAgentRegistration && !dynamicProps) { + if (this._chatAgentService.getAgentsByName(id).length) { + // Likely some extension authors will not adopt the new ID, so give a hint if they register a + // participant by name instead of ID. + throw new Error(`chatParticipant must be declared with an ID in package.json. The "id" property may be missing! "${id}"`); + } + + throw new Error(`chatParticipant must be declared in package.json: ${id}`); } const impl: IChatAgentImplementation = { @@ -107,10 +112,12 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA }; let disposable: IDisposable; - if (!staticAgentRegistration && allowDynamic) { + if (!staticAgentRegistration && dynamicProps) { disposable = this._chatAgentService.registerDynamicAgent( { - id: name, + id, + name: dynamicProps.name, + description: dynamicProps.description, extensionId: extension, metadata: revive(metadata), slashCommands: [], @@ -118,11 +125,12 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA }, impl); } else { - disposable = this._chatAgentService.registerAgent(name, impl); + disposable = this._chatAgentService.registerAgentImplementation(id, impl); } this._agents.set(handle, { - name, + id: id, + extensionId: extension, dispose: disposable.dispose, hasFollowups: metadata.hasFollowups }); @@ -134,7 +142,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA throw new Error(`No agent with handle ${handle} registered`); } data.hasFollowups = metadataUpdate.hasFollowups; - this._chatAgentService.updateAgent(data.name, revive(metadataUpdate)); + this._chatAgentService.updateAgent(data.id, revive(metadataUpdate)); } async $handleProgressChunk(requestId: string, progress: IChatProgressDto): Promise { @@ -162,8 +170,8 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA const parsedRequest = this._instantiationService.createInstance(ChatRequestParser).parseChatRequest(widget.viewModel.sessionId, model.getValue()).parts; const agentPart = parsedRequest.find((part): part is ChatRequestAgentPart => part instanceof ChatRequestAgentPart); - const thisAgentName = this._agents.get(handle)?.name; - if (agentPart?.agent.id !== thisAgentName) { + const thisAgentId = this._agents.get(handle)?.id; + if (agentPart?.agent.id !== thisAgentId) { return; } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 53b6cb09800..548f883ae97 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1427,10 +1427,14 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'mappedEditsProvider'); return extHostLanguageFeatures.registerMappedEditsProvider(extension, selector, provider); }, - createChatParticipant(name: string, handler: vscode.ChatExtendedRequestHandler) { + createChatParticipant(id: string, handler: vscode.ChatExtendedRequestHandler) { checkProposedApiEnabled(extension, 'chatParticipant'); - return extHostChatAgents2.createChatAgent(extension, name, handler); + return extHostChatAgents2.createChatAgent(extension, id, handler); }, + createDynamicChatParticipant(id: string, name: string, description: string, handler: vscode.ChatExtendedRequestHandler): vscode.ChatParticipant { + checkProposedApiEnabled(extension, 'chatParticipantAdditions'); + return extHostChatAgents2.createDynamicChatAgent(extension, id, name, description, handler); + } }; // namespace: lm diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 7ef045639f8..1aebf6615ed 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1209,7 +1209,7 @@ export interface IExtensionChatAgentMetadata extends Dto { } export interface MainThreadChatAgentsShape2 extends IDisposable { - $registerAgent(handle: number, extension: ExtensionIdentifier, name: string, metadata: IExtensionChatAgentMetadata, allowDynamic: boolean): void; + $registerAgent(handle: number, extension: ExtensionIdentifier, id: string, metadata: IExtensionChatAgentMetadata, dynamicProps: { name: string; description: string } | undefined): void; $registerAgentCompletionsProvider(handle: number, triggerCharacters: string[]): void; $unregisterAgentCompletionsProvider(handle: number): void; $updateAgent(handle: number, metadataUpdate: IExtensionChatAgentMetadata): void; diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 7a4c69dc8f3..2c5820cc2d6 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -122,7 +122,7 @@ class ChatAgentResponseStream { }, push(part) { throwIfDone(this.push); - const dto = typeConvert.ChatResponsePart.to(part); + const dto = typeConvert.ChatResponsePart.to(part, that._commandsConverter, that._sessionDisposables); _report(dto); return this; }, @@ -166,12 +166,21 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 { this._proxy = mainContext.getProxy(MainContext.MainThreadChatAgents2); } - createChatAgent(extension: IExtensionDescription, name: string, handler: vscode.ChatExtendedRequestHandler): vscode.ChatParticipant { + createChatAgent(extension: IExtensionDescription, id: string, handler: vscode.ChatExtendedRequestHandler): vscode.ChatParticipant { const handle = ExtHostChatAgents2._idPool++; - const agent = new ExtHostChatAgent(extension, name, this._proxy, handle, handler); + const agent = new ExtHostChatAgent(extension, id, this._proxy, handle, handler); this._agents.set(handle, agent); - this._proxy.$registerAgent(handle, extension.identifier, name, {}, isProposedApiEnabled(extension, 'chatParticipantAdditions')); + this._proxy.$registerAgent(handle, extension.identifier, id, {}, undefined); + return agent.apiAgent; + } + + createDynamicChatAgent(extension: IExtensionDescription, id: string, name: string, description: string, handler: vscode.ChatExtendedRequestHandler): vscode.ChatParticipant { + const handle = ExtHostChatAgents2._idPool++; + const agent = new ExtHostChatAgent(extension, id, this._proxy, handle, handler); + this._agents.set(handle, agent); + + this._proxy.$registerAgent(handle, extension.identifier, id, {}, { name, description }); return agent.apiAgent; } @@ -231,11 +240,11 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 { { ...ehResult, metadata: undefined }; // REQUEST turn - res.push(new extHostTypes.ChatRequestTurn(h.request.message, h.request.command, h.request.variables.variables.map(typeConvert.ChatAgentResolvedVariable.to), { extensionId: '', name: h.request.agentId })); + res.push(new extHostTypes.ChatRequestTurn(h.request.message, h.request.command, h.request.variables.variables.map(typeConvert.ChatAgentResolvedVariable.to), h.request.agentId)); // RESPONSE turn const parts = coalesce(h.response.map(r => typeConvert.ChatResponsePart.fromContent(r, this.commands.converter))); - res.push(new extHostTypes.ChatResponseTurn(parts, result, { extensionId: '', name: h.request.agentId }, h.request.command)); + res.push(new extHostTypes.ChatResponseTurn(parts, result, h.request.agentId, h.request.command)); } return res; @@ -338,7 +347,6 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 { class ExtHostChatAgent { private _followupProvider: vscode.ChatFollowupProvider | undefined; - private _description: string | undefined; private _fullName: string | undefined; private _iconPath: vscode.Uri | { light: vscode.Uri; dark: vscode.Uri } | vscode.ThemeIcon | undefined; private _isDefault: boolean | undefined; @@ -437,7 +445,6 @@ class ExtHostChatAgent { updateScheduled = true; queueMicrotask(() => { this._proxy.$updateAgent(this._handle, { - description: this._description, fullName: this._fullName, icon: !this._iconPath ? undefined : this._iconPath instanceof URI ? this._iconPath : @@ -463,16 +470,9 @@ class ExtHostChatAgent { const that = this; return { - get name() { + get id() { return that.id; }, - get description() { - return that._description ?? ''; - }, - set description(v) { - that._description = v; - updateMetadataSoon(); - }, get fullName() { checkProposedApiEnabled(that.extension, 'defaultChatParticipant'); return that._fullName ?? that.extension.displayName ?? that.extension.name; diff --git a/src/vs/workbench/api/common/extHostChatVariables.ts b/src/vs/workbench/api/common/extHostChatVariables.ts index e56e51676d4..26fac7e95e0 100644 --- a/src/vs/workbench/api/common/extHostChatVariables.ts +++ b/src/vs/workbench/api/common/extHostChatVariables.ts @@ -109,8 +109,13 @@ class ChatVariableResolverResponseStream { }, push(part) { throwIfDone(this.push); - const dto = typeConvert.ChatResponsePart.to(part); - _report(dto as IChatVariableResolverProgressDto); + + if (part instanceof extHostTypes.ChatResponseReferencePart) { + _report(typeConvert.ChatResponseReferencePart.to(part)); + } else if (part instanceof extHostTypes.ChatResponseProgressPart) { + _report(typeConvert.ChatResponseProgressPart.to(part)); + } + return this; } }; diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index f0d61a93f20..9a7ee3f639f 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -2454,7 +2454,7 @@ export namespace ChatResponseReferencePart { export namespace ChatResponsePart { - export function to(part: vscode.ChatResponsePart): extHostProtocol.IChatProgressDto { + export function to(part: vscode.ChatResponsePart, commandsConverter: CommandsConverter, commandDisposables: DisposableStore): extHostProtocol.IChatProgressDto { if (part instanceof types.ChatResponseMarkdownPart) { return ChatResponseMarkdownPart.to(part); } else if (part instanceof types.ChatResponseAnchorPart) { @@ -2465,6 +2465,8 @@ export namespace ChatResponsePart { return ChatResponseProgressPart.to(part); } else if (part instanceof types.ChatResponseFileTreePart) { return ChatResponseFilesPart.to(part); + } else if (part instanceof types.ChatResponseCommandButtonPart) { + return ChatResponseCommandButtonPart.to(part, commandsConverter, commandDisposables); } return { kind: 'content', @@ -2546,7 +2548,7 @@ export namespace ChatResponseProgress { }; } else if ('participant' in progress) { checkProposedApiEnabled(extension, 'chatParticipantAdditions'); - return { agentName: progress.participant, command: progress.command, kind: 'agentDetection' }; + return { agentId: progress.participant, command: progress.command, kind: 'agentDetection' }; } else if ('message' in progress) { return { content: MarkdownString.from(progress.message), kind: 'progressMessage' }; } else { diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 0480cf3372d..9d6428ebcdd 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -4310,7 +4310,7 @@ export class ChatRequestTurn implements vscode.ChatRequestTurn { readonly prompt: string, readonly command: string | undefined, readonly variables: vscode.ChatResolvedVariable[], - readonly participant: { extensionId: string; name: string }, + readonly participant: string, ) { } } @@ -4319,7 +4319,7 @@ export class ChatResponseTurn implements vscode.ChatResponseTurn { constructor( readonly response: ReadonlyArray, readonly result: vscode.ChatResult, - readonly participant: { extensionId: string; name: string }, + readonly participant: string, readonly command?: string ) { } } diff --git a/src/vs/workbench/contrib/accountEntitlements/browser/accountsEntitlements.contribution.ts b/src/vs/workbench/contrib/accountEntitlements/browser/accountsEntitlements.contribution.ts index 5acfa3942f1..4975185248d 100644 --- a/src/vs/workbench/contrib/accountEntitlements/browser/accountsEntitlements.contribution.ts +++ b/src/vs/workbench/contrib/accountEntitlements/browser/accountsEntitlements.contribution.ts @@ -148,7 +148,7 @@ class EntitlementsContribution extends Disposable implements IWorkbenchContribut } private async enableEntitlements(session: AuthenticationSession) { - const isInternal = isInternalTelemetry(this.productService, this.configurationService) ?? true; + const isInternal = isInternalTelemetry(this.productService, this.configurationService); const showAccountsBadge = this.configurationService.inspect(accountsBadgeConfigKey).value ?? false; const showWelcomeView = this.configurationService.inspect(chatWelcomeViewConfigKey).value ?? false; diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 929fb17898c..6d9c2d051df 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -128,7 +128,7 @@ export class ChatSubmitSecondaryAgentEditorAction extends EditorAction2 { if (widget.getInput().match(/^\s*@/)) { widget.acceptInput(); } else { - widget.acceptInputWithPrefix(`${chatAgentLeader}${secondaryAgent.id}`); + widget.acceptInputWithPrefix(`${chatAgentLeader}${secondaryAgent.name}`); } } } diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 33047b3b5b5..ff0d0ebc145 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -251,7 +251,7 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable { executeImmediately: true }, async (prompt, progress) => { const defaultAgent = chatAgentService.getDefaultAgent(); - const agents = chatAgentService.getRegisteredAgents(); + const agents = chatAgentService.getAgents(); // Report prefix if (defaultAgent?.metadata.helpTextPrefix) { @@ -270,7 +270,7 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable { const agentWithLeader = `${chatAgentLeader}${a.id}`; const actionArg: IChatExecuteActionContext = { inputValue: `${agentWithLeader} ${a.metadata.sampleRequest}` }; const urlSafeArg = encodeURIComponent(JSON.stringify(actionArg)); - const agentLine = `* [\`${agentWithLeader}\`](command:${SubmitAction.ID}?${urlSafeArg}) - ${a.metadata.description}`; + const agentLine = `* [\`${agentWithLeader}\`](command:${SubmitAction.ID}?${urlSafeArg}) - ${a.description}`; const commandText = a.slashCommands.map(c => { const actionArg: IChatExecuteActionContext = { inputValue: `${agentWithLeader} ${chatSubcommandLeader}${c.name} ${c.sampleRequest ?? ''}` }; const urlSafeArg = encodeURIComponent(JSON.stringify(actionArg)); diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index 00ec7e0447f..05f37e2f578 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -121,6 +121,7 @@ export interface IChatWidget { readonly providerId: string; readonly supportsFileReferences: boolean; readonly parsedInput: IParsedChatRequest; + lastSelectedAgent: IChatAgentData | undefined; getContrib(id: string): T | undefined; reveal(item: ChatTreeItem): void; diff --git a/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts b/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts index 2b358ca4519..963b28b0f5d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts @@ -3,11 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { isNonEmptyArray } from 'vs/base/common/arrays'; import { Codicon } from 'vs/base/common/codicons'; -import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { DisposableMap, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { localize, localize2 } from 'vs/nls'; import { registerAction2 } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { ILogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; @@ -20,7 +22,8 @@ import { getNewChatAction } from 'vs/workbench/contrib/chat/browser/actions/chat import { getMoveToEditorAction, getMoveToNewWindowAction } from 'vs/workbench/contrib/chat/browser/actions/chatMoveActions'; import { getQuickChatActionForProvider } from 'vs/workbench/contrib/chat/browser/actions/chatQuickInputActions'; import { CHAT_SIDEBAR_PANEL_ID, ChatViewPane, IChatViewOptions } from 'vs/workbench/contrib/chat/browser/chatViewPane'; -import { IChatContributionService, IChatParticipantContribution, IChatProviderContribution, IRawChatParticipantContribution, IRawChatProviderContribution } from 'vs/workbench/contrib/chat/common/chatContributionService'; +import { ChatAgentLocation, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatContributionService, IChatProviderContribution, IRawChatParticipantContribution, IRawChatProviderContribution } from 'vs/workbench/contrib/chat/common/chatContributionService'; import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import * as extensionsRegistry from 'vs/workbench/services/extensions/common/extensionsRegistry'; @@ -64,20 +67,24 @@ const chatExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensi const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint({ extensionPoint: 'chatParticipants', jsonSchema: { - description: localize('vscode.extension.contributes.chatParticipant', 'Contributes a Chat Participant'), + description: localize('vscode.extension.contributes.chatParticipant', 'Contributes a chat participant'), type: 'array', items: { additionalProperties: false, type: 'object', defaultSnippets: [{ body: { name: '', description: '' } }], - required: ['name'], + required: ['name', 'id'], properties: { + id: { + description: localize('chatParticipantId', "A unique id for this chat participant."), + type: 'string' + }, name: { - description: localize('chatParticipantName', "Unique name for this Chat Participant."), + description: localize('chatParticipantName', "User-facing display name for this chat participant. The user will use '@' with this name to invoke the participant."), type: 'string' }, description: { - description: localize('chatParticipantDescription', "A description of this Chat Participant, shown in the UI."), + description: localize('chatParticipantDescription', "A description of this chat participant, shown in the UI."), type: 'string' }, isDefault: { @@ -92,7 +99,7 @@ const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.regi } }, commands: { - markdownDescription: localize('chatCommandsDescription', "Commands available for this Chat Participant, which the user can invoke with a `/`."), + markdownDescription: localize('chatCommandsDescription', "Commands available for this chat participant, which the user can invoke with a `/`."), type: 'array', items: { additionalProperties: false, @@ -131,7 +138,7 @@ const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.regi } }, locations: { - markdownDescription: localize('chatLocationsDescription', "Locations in which this Chat Participant is available."), + markdownDescription: localize('chatLocationsDescription', "Locations in which this chat participant is available."), type: 'array', default: ['panel'], items: { @@ -158,12 +165,14 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { private _welcomeViewDescriptor?: IViewDescriptor; private _viewContainer: ViewContainer; private _registrationDisposables = new Map(); + private _participantRegistrationDisposables = new DisposableMap(); constructor( - @IChatContributionService readonly _chatContributionService: IChatContributionService, - @IProductService readonly productService: IProductService, - @IContextKeyService readonly contextService: IContextKeyService, - @ILogService readonly logService: ILogService, + @IChatContributionService private readonly _chatContributionService: IChatContributionService, + @IChatAgentService private readonly _chatAgentService: IChatAgentService, + @IProductService private readonly productService: IProductService, + @IContextKeyService private readonly contextService: IContextKeyService, + @ILogService private readonly logService: ILogService, ) { this._viewContainer = this.registerViewContainer(); this.registerListeners(); @@ -243,13 +252,34 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { continue; } - this._chatContributionService.registerChatParticipant({ ...providerDescriptor, extensionId: extension.description.identifier }); + if (!providerDescriptor.id || !providerDescriptor.name) { + this.logService.error(`Extension '${extension.description.identifier.value}' CANNOT register participant without both id and name.`); + continue; + } + + this._participantRegistrationDisposables.set( + getParticipantKey(extension.description.identifier, providerDescriptor.name), + this._chatAgentService.registerAgent( + providerDescriptor.id, + { + extensionId: extension.description.identifier, + id: providerDescriptor.id, + description: providerDescriptor.description, + metadata: {}, + name: providerDescriptor.name, + isDefault: providerDescriptor.isDefault, + defaultImplicitVariables: providerDescriptor.defaultImplicitVariables, + locations: isNonEmptyArray(providerDescriptor.locations) ? + providerDescriptor.locations.map(ChatAgentLocation.fromRaw) : + [ChatAgentLocation.Panel], + slashCommands: providerDescriptor.commands ?? [] + } satisfies IChatAgentData)); } } for (const extension of delta.removed) { for (const providerDescriptor of extension.value) { - this._chatContributionService.deregisterChatParticipant({ ...providerDescriptor, extensionId: extension.description.identifier }); + this._participantRegistrationDisposables.deleteAndDispose(getParticipantKey(extension.description.identifier, providerDescriptor.name)); } } }); @@ -314,15 +344,14 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { registerWorkbenchContribution2(ChatExtensionPointHandler.ID, ChatExtensionPointHandler, WorkbenchPhase.BlockStartup); -function getParticipantKey(participant: IChatParticipantContribution): string { - return `${participant.extensionId.value}_${participant.name}`; +function getParticipantKey(extensionId: ExtensionIdentifier, participantName: string): string { + return `${extensionId.value}_${participantName}`; } export class ChatContributionService implements IChatContributionService { declare _serviceBrand: undefined; private _registeredProviders = new Map(); - private _registeredParticipants = new Map(); constructor( ) { } @@ -339,19 +368,7 @@ export class ChatContributionService implements IChatContributionService { this._registeredProviders.delete(providerId); } - public registerChatParticipant(participant: IChatParticipantContribution): void { - this._registeredParticipants.set(getParticipantKey(participant), participant); - } - - public deregisterChatParticipant(participant: IChatParticipantContribution): void { - this._registeredParticipants.delete(getParticipantKey(participant)); - } - public get registeredProviders(): IChatProviderContribution[] { return Array.from(this._registeredProviders.values()); } - - public get registeredParticipants(): IChatParticipantContribution[] { - return Array.from(this._registeredParticipants.values()); - } } diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 6ce90ab6de8..28758e10791 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -360,7 +360,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer d.value).find((d): d is URI => d instanceof URI) || undefined; - const title = uri ? encodeURIComponent(this.labelService.getUriLabel(uri, { relative: true })) : ''; + const title = uri ? encodeURIComponent(this.labelService.getUriLabel(uri, { relative: true })) : + part instanceof ChatRequestAgentPart ? part.agent.id : + ''; result += `[${part.text}](${variableRefUrl}?${title})`; } @@ -106,4 +108,3 @@ export class ChatMarkdownDecorationsRenderer { } } } - diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 8eaa0d669de..df3bf9fde56 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -145,7 +145,7 @@ export class ChatWidget extends Disposable implements IChatWidget { private parsedChatRequest: IParsedChatRequest | undefined; get parsedInput() { if (this.parsedChatRequest === undefined) { - this.parsedChatRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(this.viewModel!.sessionId, this.getInput(), this.location); + this.parsedChatRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(this.viewModel!.sessionId, this.getInput(), this.location, { selectedAgent: this._lastSelectedAgent }); } return this.parsedChatRequest; @@ -213,6 +213,15 @@ export class ChatWidget extends Disposable implements IChatWidget { })); } + private _lastSelectedAgent: IChatAgentData | undefined; + set lastSelectedAgent(agent: IChatAgentData | undefined) { + this._lastSelectedAgent = agent; + } + + get lastSelectedAgent(): IChatAgentData | undefined { + return this._lastSelectedAgent; + } + get supportsFileReferences(): boolean { return !!this.viewOptions.supportsFileReferences; } @@ -673,7 +682,7 @@ export class ChatWidget extends Disposable implements IChatWidget { 'query' in opts ? opts.query : `${opts.prefix} ${editorValue}`; const isUserQuery = !opts || 'prefix' in opts; - const result = await this.chatService.sendRequest(this.viewModel.sessionId, input, this.inputPart.implicitContextEnabled, this.location); + const result = await this.chatService.sendRequest(this.viewModel.sessionId, input, this.inputPart.implicitContextEnabled, this.location, { selectedAgent: this._lastSelectedAgent }); if (result) { const inputState = this.collectInputState(); diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts index f4d19e3955e..167728b3947 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from 'vs/base/common/cancellation'; +import { MarkdownString } from 'vs/base/common/htmlContent'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { Position } from 'vs/editor/common/core/position'; @@ -14,7 +15,8 @@ import { CompletionContext, CompletionItem, CompletionItemKind, CompletionList } import { ITextModel } from 'vs/editor/common/model'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { localize } from 'vs/nls'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { Registry } from 'vs/platform/registry/common/platform'; import { inputPlaceholderForeground } from 'vs/platform/theme/common/colorRegistry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -37,8 +39,8 @@ const placeholderDecorationType = 'chat-session-detail'; const slashCommandTextDecorationType = 'chat-session-text'; const variableTextDecorationType = 'chat-variable-text'; -function agentAndCommandToKey(agent: string, subcommand: string | undefined): string { - return subcommand ? `${agent}__${subcommand}` : agent; +function agentAndCommandToKey(agent: IChatAgentData, subcommand: string | undefined): string { + return subcommand ? `${agent.id}__${subcommand}` : agent.id; } class InputEditorDecorations extends Disposable { @@ -70,7 +72,7 @@ class InputEditorDecorations extends Disposable { this.updateInputEditorDecorations(); })); this._register(this.widget.onDidSubmitAgent((e) => { - this.previouslyUsedAgents.add(agentAndCommandToKey(e.agent.id, e.slashCommand?.name)); + this.previouslyUsedAgents.add(agentAndCommandToKey(e.agent, e.slashCommand?.name)); })); this._register(this.chatAgentService.onDidChangeAgents(() => this.updateInputEditorDecorations())); @@ -135,7 +137,7 @@ class InputEditorDecorations extends Disposable { }, renderOptions: { after: { - contentText: viewModel.inputPlaceholder ?? defaultAgent?.metadata.description ?? '', + contentText: viewModel.inputPlaceholder ?? defaultAgent?.description ?? '', color: this.getPlaceholderColor() } } @@ -172,14 +174,14 @@ class InputEditorDecorations extends Disposable { const onlyAgentAndWhitespace = agentPart && parsedRequest.every(p => p instanceof ChatRequestTextPart && !p.text.trim().length || p instanceof ChatRequestAgentPart); if (onlyAgentAndWhitespace) { // Agent reference with no other text - show the placeholder - const isFollowupSlashCommand = this.previouslyUsedAgents.has(agentAndCommandToKey(agentPart.agent.id, undefined)); + const isFollowupSlashCommand = this.previouslyUsedAgents.has(agentAndCommandToKey(agentPart.agent, undefined)); const shouldRenderFollowupPlaceholder = isFollowupSlashCommand && agentPart.agent.metadata.followupPlaceholder; - if (agentPart.agent.metadata.description && exactlyOneSpaceAfterPart(agentPart)) { + if (agentPart.agent.description && exactlyOneSpaceAfterPart(agentPart)) { placeholderDecoration = [{ range: getRangeForPlaceholder(agentPart), renderOptions: { after: { - contentText: shouldRenderFollowupPlaceholder ? agentPart.agent.metadata.followupPlaceholder : agentPart.agent.metadata.description, + contentText: shouldRenderFollowupPlaceholder ? agentPart.agent.metadata.followupPlaceholder : agentPart.agent.description, color: this.getPlaceholderColor(), } } @@ -190,7 +192,7 @@ class InputEditorDecorations extends Disposable { const onlyAgentCommandAndWhitespace = agentPart && agentSubcommandPart && parsedRequest.every(p => p instanceof ChatRequestTextPart && !p.text.trim().length || p instanceof ChatRequestAgentPart || p instanceof ChatRequestAgentSubcommandPart); if (onlyAgentCommandAndWhitespace) { // Agent reference and subcommand with no other text - show the placeholder - const isFollowupSlashCommand = this.previouslyUsedAgents.has(agentAndCommandToKey(agentPart.agent.id, agentSubcommandPart.command.name)); + const isFollowupSlashCommand = this.previouslyUsedAgents.has(agentAndCommandToKey(agentPart.agent, agentSubcommandPart.command.name)); const shouldRenderFollowupPlaceholder = isFollowupSlashCommand && agentSubcommandPart.command.followupPlaceholder; if (agentSubcommandPart?.command.description && exactlyOneSpaceAfterPart(agentSubcommandPart)) { placeholderDecoration = [{ @@ -209,9 +211,10 @@ class InputEditorDecorations extends Disposable { const textDecorations: IDecorationOptions[] | undefined = []; if (agentPart) { - textDecorations.push({ range: agentPart.editorRange }); + const agentHover = `(${agentPart.agent.id}) ${agentPart.agent.description}`; + textDecorations.push({ range: agentPart.editorRange, hoverMessage: new MarkdownString(agentHover) }); if (agentSubcommandPart) { - textDecorations.push({ range: agentSubcommandPart.editorRange }); + textDecorations.push({ range: agentSubcommandPart.editorRange, hoverMessage: new MarkdownString(agentSubcommandPart.command.description) }); } } @@ -246,9 +249,9 @@ class InputEditorSlashCommandMode extends Disposable { private async repopulateAgentCommand(agent: IChatAgentData, slashCommand: IChatAgentCommand | undefined) { let value: string | undefined; if (slashCommand && slashCommand.isSticky) { - value = `${chatAgentLeader}${agent.id} ${chatSubcommandLeader}${slashCommand.name} `; + value = `${chatAgentLeader}${agent.name} ${chatSubcommandLeader}${slashCommand.name} `; } else if (agent.metadata.isSticky) { - value = `${chatAgentLeader}${agent.id} `; + value = `${chatAgentLeader}${agent.name} `; } if (value) { @@ -349,13 +352,18 @@ class AgentCompletions extends Disposable { .filter(a => a.locations.includes(widget.location)); return { - suggestions: agents.map((c, i) => { - const withAt = `@${c.id}`; + suggestions: agents.map((a, i) => { + const withAt = `@${a.name}`; + const isDupe = !!agents.find(other => other.name === a.name && other.id !== a.id); return { - label: withAt, + // Leading space is important because detail has no space at the start by design + label: isDupe ? + { label: withAt, description: a.description, detail: ` (${a.id})` } : + withAt, insertText: `${withAt} `, - detail: c.metadata.description, + detail: a.description, range: new Range(1, 1, 1, 1), + command: { id: AssignSelectedAgentAction.ID, title: AssignSelectedAgentAction.ID, arguments: [{ agent: a, widget } satisfies AssignSelectedAgentActionArgs] }, kind: CompletionItemKind.Text, // The icons are disabled here anyway }; }) @@ -435,31 +443,37 @@ class AgentCompletions extends Disposable { const justAgents: CompletionItem[] = agents .filter(a => !a.isDefault) .map(agent => { - const agentLabel = `${chatAgentLeader}${agent.id}`; + const isDupe = !!agents.find(other => other.name === agent.name && other.id !== agent.id); + const detail = agent.description; + const agentLabel = `${chatAgentLeader}${agent.name} (${agent.id})`; + return { - label: { label: agentLabel, description: agent.metadata.description }, - filterText: `${chatSubcommandLeader}${agent.id}`, + label: isDupe ? + { label: agentLabel, description: agent.description, detail: ` (${agent.id})` } : + agentLabel, + detail, + filterText: `${chatSubcommandLeader}${agent.name}`, insertText: `${agentLabel} `, range: new Range(1, 1, 1, 1), kind: CompletionItemKind.Text, - sortText: `${chatSubcommandLeader}${agent.id}`, + sortText: `${chatSubcommandLeader}${agent.name}`, }; }); return { suggestions: justAgents.concat( agents.flatMap(agent => agent.slashCommands.map((c, i) => { - const agentLabel = `${chatAgentLeader}${agent.id}`; + const agentLabel = `${chatAgentLeader}${agent.name}`; const withSlash = `${chatSubcommandLeader}${c.name}`; return { label: { label: withSlash, description: agentLabel }, - filterText: `${chatSubcommandLeader}${agent.id}${c.name}`, + filterText: `${chatSubcommandLeader}${agent.name}${c.name}`, commitCharacters: [' '], insertText: `${agentLabel} ${withSlash} `, detail: `(${agentLabel}) ${c.description ?? ''}`, range: new Range(1, 1, 1, 1), kind: CompletionItemKind.Text, // The icons are disabled here anyway - sortText: `${chatSubcommandLeader}${agent.id}${c.name}`, + sortText: `${chatSubcommandLeader}${agent.name}${c.name}`, } satisfies CompletionItem; }))) }; @@ -469,6 +483,32 @@ class AgentCompletions extends Disposable { } Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(AgentCompletions, LifecyclePhase.Eventually); +interface AssignSelectedAgentActionArgs { + agent: IChatAgentData; + widget: IChatWidget; +} + +class AssignSelectedAgentAction extends Action2 { + static readonly ID = 'workbench.action.chat.assignSelectedAgent'; + + constructor() { + super({ + id: AssignSelectedAgentAction.ID, + title: '' // not displayed + }); + } + + async run(accessor: ServicesAccessor, ...args: any[]) { + const arg: AssignSelectedAgentActionArgs = args[0]; + if (!arg || !arg.widget || !arg.agent) { + return; + } + + arg.widget.lastSelectedAgent = arg.agent; + } +} +registerAction2(AssignSelectedAgentAction); + class BuiltinDynamicCompletions extends Disposable { private static readonly VariableNameDef = new RegExp(`${chatVariableLeader}\\w*`, 'g'); // MUST be using `g`-flag @@ -596,12 +636,14 @@ class ChatTokenDeleter extends Disposable { const parser = this.instantiationService.createInstance(ChatRequestParser); const inputValue = this.widget.inputEditor.getValue(); let previousInputValue: string | undefined; + let previousSelectedAgent: IChatAgentData | undefined; // A simple heuristic to delete the previous token when the user presses backspace. // The sophisticated way to do this would be to have a parse tree that can be updated incrementally. this._register(this.widget.inputEditor.onDidChangeModelContent(e => { if (!previousInputValue) { previousInputValue = inputValue; + previousSelectedAgent = this.widget.lastSelectedAgent; } // Don't try to handle multicursor edits right now @@ -609,7 +651,7 @@ class ChatTokenDeleter extends Disposable { // If this was a simple delete, try to find out whether it was inside a token if (!change.text && this.widget.viewModel) { - const previousParsedValue = parser.parseChatRequest(this.widget.viewModel.sessionId, previousInputValue); + const previousParsedValue = parser.parseChatRequest(this.widget.viewModel.sessionId, previousInputValue, { selectedAgent: previousSelectedAgent }); // For dynamic variables, this has to happen in ChatDynamicVariableModel with the other bookkeeping const deletableTokens = previousParsedValue.parts.filter(p => p instanceof ChatRequestAgentPart || p instanceof ChatRequestAgentSubcommandPart || p instanceof ChatRequestSlashCommandPart || p instanceof ChatRequestVariablePart); @@ -629,6 +671,7 @@ class ChatTokenDeleter extends Disposable { } previousInputValue = this.widget.inputEditor.getValue(); + previousSelectedAgent = this.widget.lastSelectedAgent; })); } } diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index eddd86a969e..b15789713ba 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -506,6 +506,7 @@ .quick-input-widget .interactive-session .interactive-input-part { padding: 8px 6px 6px 6px; + margin: 0 3px; } .quick-input-widget .interactive-session .interactive-input-part .interactive-execute-toolbar { diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index 6175cea0429..203aa9548de 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -3,19 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { isNonEmptyArray, distinct } from 'vs/base/common/arrays'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { Iterable } from 'vs/base/common/iterator'; -import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; import { ProviderResult } from 'vs/editor/common/languages'; import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IChatContributionService, IRawChatCommandContribution, RawChatParticipantLocation } from 'vs/workbench/contrib/chat/common/chatContributionService'; +import { IRawChatCommandContribution, RawChatParticipantLocation } from 'vs/workbench/contrib/chat/common/chatContributionService'; import { IChatProgressResponseContent, IChatRequestVariableData } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChatFollowup, IChatProgress, IChatResponseErrorDetails } from 'vs/workbench/contrib/chat/common/chatService'; @@ -47,6 +46,8 @@ export namespace ChatAgentLocation { export interface IChatAgentData { id: string; + name: string; + description?: string; extensionId: ExtensionIdentifier; /** The agent invoked when no agent is specified */ isDefault?: boolean; @@ -79,7 +80,6 @@ export interface IChatRequesterInformation { } export interface IChatAgentMetadata { - description?: string; helpTextPrefix?: string | IMarkdownString; helpTextVariablesPrefix?: string | IMarkdownString; helpTextPostfix?: string | IMarkdownString; @@ -118,86 +118,102 @@ export interface IChatAgentResult { export const IChatAgentService = createDecorator('chatAgentService'); +interface IChatAgentEntry { + data: IChatAgentData; + impl?: IChatAgentImplementation; +} + export interface IChatAgentService { _serviceBrand: undefined; /** * undefined when an agent was removed IChatAgent */ readonly onDidChangeAgents: Event; - registerAgent(name: string, agent: IChatAgentImplementation): IDisposable; + registerAgent(id: string, data: IChatAgentData): IDisposable; + registerAgentImplementation(id: string, agent: IChatAgentImplementation): IDisposable; registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable; - invokeAgent(id: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; + invokeAgent(agent: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; - getAgents(): IChatAgentData[]; - getRegisteredAgents(): Array; - getActivatedAgents(): Array; getAgent(id: string): IChatAgentData | undefined; + getAgents(): IChatAgentData[]; + getActivatedAgents(): Array; + getAgentsByName(name: string): IChatAgentData[]; getDefaultAgent(): IChatAgent | undefined; getSecondaryAgent(): IChatAgentData | undefined; updateAgent(id: string, updateMetadata: IChatAgentMetadata): void; } -export class ChatAgentService extends Disposable implements IChatAgentService { +export class ChatAgentService implements IChatAgentService { public static readonly AGENT_LEADER = '@'; declare _serviceBrand: undefined; - private readonly _agents = new Map(); + private _agents: IChatAgentEntry[] = []; - private readonly _onDidChangeAgents = this._register(new Emitter()); + private readonly _onDidChangeAgents = new Emitter(); readonly onDidChangeAgents: Event = this._onDidChangeAgents.event; constructor( - @IChatContributionService private chatContributionService: IChatContributionService, - @IContextKeyService private contextKeyService: IContextKeyService, - ) { - super(); - } + @IContextKeyService private readonly contextKeyService: IContextKeyService + ) { } - override dispose(): void { - super.dispose(); - this._agents.clear(); - } - - registerAgent(name: string, agentImpl: IChatAgentImplementation): IDisposable { - if (this._agents.has(name)) { - // TODO not keyed by name, dupes allowed between extensions - throw new Error(`Already registered an agent with id ${name}`); + registerAgent(id: string, data: IChatAgentData): IDisposable { + const existingAgent = this.getAgent(id); + if (existingAgent) { + throw new Error(`Agent already registered: ${JSON.stringify(id)}`); } - const data = this.getAgent(name); - if (!data) { - throw new Error(`Unknown agent: ${name}`); + const that = this; + const commands = data.slashCommands; + data = { + ...data, + get slashCommands() { + return commands.filter(c => !c.when || that.contextKeyService.contextMatchesRules(ContextKeyExpr.deserialize(c.when))); + } + }; + const entry = { data }; + this._agents.push(entry); + return toDisposable(() => { + this._agents = this._agents.filter(a => a !== entry); + this._onDidChangeAgents.fire(undefined); + }); + } + + registerAgentImplementation(id: string, agentImpl: IChatAgentImplementation): IDisposable { + const entry = this._getAgentEntry(id); + if (!entry) { + throw new Error(`Unknown agent: ${JSON.stringify(id)}`); } - const agent = { data: data, impl: agentImpl }; - this._agents.set(name, agent); - this._onDidChangeAgents.fire(new MergedChatAgent(data, agentImpl)); + if (entry.impl) { + throw new Error(`Agent already has implementation: ${JSON.stringify(id)}`); + } + + entry.impl = agentImpl; + this._onDidChangeAgents.fire(new MergedChatAgent(entry.data, agentImpl)); return toDisposable(() => { - if (this._agents.delete(name)) { - this._onDidChangeAgents.fire(undefined); - } + entry.impl = undefined; + this._onDidChangeAgents.fire(undefined); }); } registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable { const agent = { data, impl: agentImpl }; - this._agents.set(data.id, agent); + this._agents.push(agent); this._onDidChangeAgents.fire(new MergedChatAgent(data, agentImpl)); return toDisposable(() => { - if (this._agents.delete(data.id)) { - this._onDidChangeAgents.fire(undefined); - } + this._agents = this._agents.filter(a => a !== agent); + this._onDidChangeAgents.fire(undefined); }); } updateAgent(id: string, updateMetadata: IChatAgentMetadata): void { - const agent = this._agents.get(id); + const agent = this._getAgentEntry(id); if (!agent?.impl) { - throw new Error(`No activated agent with id ${id} registered`); + throw new Error(`No activated agent with id ${JSON.stringify(id)} registered`); } agent.data.metadata = { ...agent.data.metadata, ...updateMetadata }; this._onDidChangeAgents.fire(new MergedChatAgent(agent.data, agent.impl)); @@ -213,35 +229,19 @@ export class ChatAgentService extends Disposable implements IChatAgentService { return Iterable.find(this._agents.values(), a => !!a.data.metadata.isSecondary)?.data; } - getRegisteredAgents(): Array { - const that = this; - return this.chatContributionService.registeredParticipants.map(p => ( - { - extensionId: p.extensionId, - id: p.name, - metadata: this._agents.has(p.name) ? this._agents.get(p.name)!.data.metadata : { description: p.description }, - isDefault: p.isDefault, - defaultImplicitVariables: p.defaultImplicitVariables, - locations: isNonEmptyArray(p.locations) ? p.locations.map(ChatAgentLocation.fromRaw) : [ChatAgentLocation.Panel], - get slashCommands() { - const commands = p.commands ?? []; - return commands.filter(c => !c.when || that.contextKeyService.contextMatchesRules(ContextKeyExpr.deserialize(c.when))); - } - } satisfies IChatAgentData)); + private _getAgentEntry(id: string): IChatAgentEntry | undefined { + return this._agents.find(a => a.data.id === id); + } + + getAgent(id: string): IChatAgentData | undefined { + return this._getAgentEntry(id)?.data; } /** * Returns all agent datas that exist- static registered and dynamic ones. */ getAgents(): IChatAgentData[] { - const registeredAgents = this.getRegisteredAgents(); - const dynamicAgents = Array.from(this._agents.values()).map(a => a.data); - const all = [ - ...registeredAgents, - ...dynamicAgents - ]; - - return distinct(all, a => a.id); + return this._agents.map(entry => entry.data); } getActivatedAgents(): IChatAgent[] { @@ -250,12 +250,12 @@ export class ChatAgentService extends Disposable implements IChatAgentService { .map(a => new MergedChatAgent(a.data, a.impl!)); } - getAgent(id: string): IChatAgentData | undefined { - return this.getAgents().find(a => a.id === id); + getAgentsByName(name: string): IChatAgentData[] { + return this.getAgents().filter(a => a.name === name); } async invokeAgent(id: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { - const data = this._agents.get(id); + const data = this._getAgentEntry(id); if (!data?.impl) { throw new Error(`No activated agent with id ${id}`); } @@ -264,7 +264,7 @@ export class ChatAgentService extends Disposable implements IChatAgentService { } async getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { - const data = this._agents.get(id); + const data = this._getAgentEntry(id); if (!data?.impl) { throw new Error(`No activated agent with id ${id}`); } @@ -284,6 +284,8 @@ export class MergedChatAgent implements IChatAgent { ) { } get id(): string { return this.data.id; } + get name(): string { return this.data.name ?? ''; } + get description(): string { return this.data.description ?? ''; } get extensionId(): ExtensionIdentifier { return this.data.extensionId; } get isDefault(): boolean | undefined { return this.data.isDefault; } get metadata(): IChatAgentMetadata { return this.data.metadata; } diff --git a/src/vs/workbench/contrib/chat/common/chatContributionService.ts b/src/vs/workbench/contrib/chat/common/chatContributionService.ts index b0e4facf10c..aaef3c6aacb 100644 --- a/src/vs/workbench/contrib/chat/common/chatContributionService.ts +++ b/src/vs/workbench/contrib/chat/common/chatContributionService.ts @@ -19,10 +19,6 @@ export interface IChatContributionService { registerChatProvider(provider: IChatProviderContribution): void; deregisterChatProvider(providerId: string): void; getViewIdForProvider(providerId: string): string; - - readonly registeredParticipants: IChatParticipantContribution[]; - registerChatParticipant(participant: IChatParticipantContribution): void; - deregisterChatParticipant(participant: IChatParticipantContribution): void; } export interface IRawChatProviderContribution { @@ -44,6 +40,7 @@ export interface IRawChatCommandContribution { export type RawChatParticipantLocation = 'panel' | 'terminal' | 'notebook'; export interface IRawChatParticipantContribution { + id: string; name: string; description?: string; isDefault?: boolean; diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index f8d0feff438..efba768f7ba 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -617,7 +617,7 @@ export class ChatModel extends Disposable implements IChatModel { const request = new ChatRequestModel(this, parsedRequest, variableData); if (raw.response || raw.result || (raw as any).responseErrorDetails) { const agent = (raw.agent && 'metadata' in raw.agent) ? // Check for the new format, ignore entries in the old format - revive(raw.agent) : undefined; + this.reviveSerializedAgent(raw.agent) : undefined; // Port entries from old format const result = 'responseErrorDetails' in raw ? @@ -639,6 +639,16 @@ export class ChatModel extends Disposable implements IChatModel { } } + private reviveSerializedAgent(raw: ISerializableChatAgentData): IChatAgentData { + const agent = 'name' in raw ? + raw : + { + ...(raw as any), + name: (raw as any).id, + }; + return revive(agent); + } + private getParsedRequestFromString(message: string): IParsedChatRequest { // TODO These offsets won't be used, but chat replies need to go through the parser as well const parts = [new ChatRequestTextPart(new OffsetRange(0, message.length), { startColumn: 1, startLineNumber: 1, endColumn: 1, endLineNumber: 1 }, message)]; @@ -729,7 +739,7 @@ export class ChatModel extends Disposable implements IChatModel { } else if (progress.kind === 'usedContext' || progress.kind === 'reference') { request.response.applyReference(progress); } else if (progress.kind === 'agentDetection') { - const agent = this.chatAgentService.getAgent(progress.agentName); + const agent = this.chatAgentService.getAgent(progress.agentId); if (agent) { request.response.setAgent(agent, progress.command, progress.detected); } @@ -830,7 +840,7 @@ export class ChatModel extends Disposable implements IChatModel { vote: r.response?.vote, agent: r.response?.agent ? // May actually be the full IChatAgent instance, just take the data props. slashCommands don't matter here. - { id: r.response.agent.id, extensionId: r.response.agent.extensionId, metadata: r.response.agent.metadata, slashCommands: [], locations: r.response.agent.locations, isDefault: r.response.agent.isDefault } + { id: r.response.agent.id, name: r.response.agent.name, description: r.response.agent.description, extensionId: r.response.agent.extensionId, metadata: r.response.agent.metadata, slashCommands: [], locations: r.response.agent.locations, isDefault: r.response.agent.isDefault } : undefined, slashCommand: r.response?.slashCommand, usedContext: r.response?.usedContext, diff --git a/src/vs/workbench/contrib/chat/common/chatParserTypes.ts b/src/vs/workbench/contrib/chat/common/chatParserTypes.ts index 59f8b873cbb..7edda68e10e 100644 --- a/src/vs/workbench/contrib/chat/common/chatParserTypes.ts +++ b/src/vs/workbench/contrib/chat/common/chatParserTypes.ts @@ -75,7 +75,7 @@ export class ChatRequestAgentPart implements IParsedChatRequestPart { constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly agent: IChatAgentData) { } get text(): string { - return `${chatAgentLeader}${this.agent.id}`; + return `${chatAgentLeader}${this.agent.name}`; } get promptText(): string { @@ -92,6 +92,8 @@ export class ChatRequestAgentPart implements IParsedChatRequestPart { editorRange: this.editorRange, agent: { id: this.agent.id, + name: this.agent.name, + description: this.agent.description, metadata: this.agent.metadata } }; @@ -167,10 +169,19 @@ export function reviveParsedChatRequest(serialized: IParsedChatRequest): IParsed (part as ChatRequestVariablePart).variableArg ); } else if (part.kind === ChatRequestAgentPart.Kind) { + let agent = (part as ChatRequestAgentPart).agent; + if (!('name' in agent)) { + // Port old format + agent = { + ...(agent as any), + name: (agent as any).id + }; + } + return new ChatRequestAgentPart( new OffsetRange(part.range.start, part.range.endExclusive), part.editorRange, - (part as ChatRequestAgentPart).agent + agent ); } else if (part.kind === ChatRequestAgentSubcommandPart.Kind) { return new ChatRequestAgentSubcommandPart( diff --git a/src/vs/workbench/contrib/chat/common/chatRequestParser.ts b/src/vs/workbench/contrib/chat/common/chatRequestParser.ts index d8c4d5299fc..f60b9cb5dc3 100644 --- a/src/vs/workbench/contrib/chat/common/chatRequestParser.ts +++ b/src/vs/workbench/contrib/chat/common/chatRequestParser.ts @@ -6,10 +6,8 @@ import { OffsetRange } from 'vs/editor/common/core/offsetRange'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { ChatAgentLocation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { IChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatAgentLocation, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestDynamicVariablePart, ChatRequestSlashCommandPart, ChatRequestTextPart, ChatRequestVariablePart, IParsedChatRequest, IParsedChatRequestPart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; import { IChatVariablesService, IDynamicVariable } from 'vs/workbench/contrib/chat/common/chatVariables'; @@ -17,19 +15,21 @@ const agentReg = /^@([\w_\-]+)(?=(\s|$|\b))/i; // An @-agent const variableReg = /^#([\w_\-]+)(:\d+)?(?=(\s|$|\b))/i; // A #-variable with an optional numeric : arg (@response:2) const slashReg = /\/([\w_\-]+)(?=(\s|$|\b))/i; // A / command +export interface IChatParserContext { + /** Used only as a disambiguator, when the query references an agent that has a duplicate with the same name. */ + selectedAgent?: IChatAgentData; +} + export class ChatRequestParser { constructor( @IChatAgentService private readonly agentService: IChatAgentService, @IChatVariablesService private readonly variableService: IChatVariablesService, - @IChatSlashCommandService private readonly slashCommandService: IChatSlashCommandService, - @IChatService private readonly chatService: IChatService + @IChatSlashCommandService private readonly slashCommandService: IChatSlashCommandService ) { } - parseChatRequest(sessionId: string, message: string, location: ChatAgentLocation = ChatAgentLocation.Panel): IParsedChatRequest { + parseChatRequest(sessionId: string, message: string, location: ChatAgentLocation = ChatAgentLocation.Panel, context?: IChatParserContext): IParsedChatRequest { const parts: IParsedChatRequestPart[] = []; const references = this.variableService.getDynamicVariables(sessionId); // must access this list before any async calls - const model = this.chatService.getSession(sessionId)!; - // TODO@jrieken put location on IChatModel let lineNumber = 1; let column = 1; @@ -41,9 +41,9 @@ export class ChatRequestParser { if (char === chatVariableLeader) { newPart = this.tryToParseVariable(message.slice(i), i, new Position(lineNumber, column), parts); } else if (char === chatAgentLeader) { - newPart = this.tryToParseAgent(message.slice(i), message, i, new Position(lineNumber, column), parts, location); + newPart = this.tryToParseAgent(message.slice(i), message, i, new Position(lineNumber, column), parts, location, context); } else if (char === chatSubcommandLeader) { - newPart = this.tryToParseSlashCommand(model, message.slice(i), message, i, new Position(lineNumber, column), parts); + newPart = this.tryToParseSlashCommand(message.slice(i), message, i, new Position(lineNumber, column), parts); } if (!newPart) { @@ -90,17 +90,23 @@ export class ChatRequestParser { }; } - private tryToParseAgent(message: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray, location: ChatAgentLocation): ChatRequestAgentPart | ChatRequestVariablePart | undefined { - const nextVariableMatch = message.match(agentReg); - if (!nextVariableMatch) { + private tryToParseAgent(message: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray, location: ChatAgentLocation, context: IChatParserContext | undefined): ChatRequestAgentPart | ChatRequestVariablePart | undefined { + const nextAgentMatch = message.match(agentReg); + if (!nextAgentMatch) { return; } - const [full, name] = nextVariableMatch; - const varRange = new OffsetRange(offset, offset + full.length); - const varEditorRange = new Range(position.lineNumber, position.column, position.lineNumber, position.column + full.length); + const [full, name] = nextAgentMatch; + const agentRange = new OffsetRange(offset, offset + full.length); + const agentEditorRange = new Range(position.lineNumber, position.column, position.lineNumber, position.column + full.length); - const agent = this.agentService.getAgent(name); + const agents = this.agentService.getAgentsByName(name); + + // If there is more than one agent with this name, and the user picked it from the suggest widget, then the selected agent should be in the + // context and we use that one. Otherwise just pick the first. + const agent = agents.length > 1 && context?.selectedAgent ? + context.selectedAgent : + agents[0]; if (!agent || !agent.locations.includes(location)) { return; } @@ -122,7 +128,7 @@ export class ChatRequestParser { return; } - return new ChatRequestAgentPart(varRange, varEditorRange, agent); + return new ChatRequestAgentPart(agentRange, agentEditorRange, agent); } private tryToParseVariable(message: string, offset: number, position: IPosition, parts: ReadonlyArray): ChatRequestAgentPart | ChatRequestVariablePart | undefined { @@ -143,7 +149,7 @@ export class ChatRequestParser { return; } - private tryToParseSlashCommand(model: IChatModel, remainingMessage: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray): ChatRequestSlashCommandPart | ChatRequestAgentSubcommandPart | undefined { + private tryToParseSlashCommand(remainingMessage: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray): ChatRequestSlashCommandPart | ChatRequestAgentSubcommandPart | undefined { const nextSlashMatch = remainingMessage.match(slashReg); if (!nextSlashMatch) { return; diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 69cb00233b9..da68a68451c 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -15,6 +15,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatModel, IChatModel, IChatRequestVariableData, ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import { IChatParserContext } from 'vs/workbench/contrib/chat/common/chatRequestParser'; import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables'; export interface IChat { @@ -85,7 +86,7 @@ export interface IChatContentInlineReference { } export interface IChatAgentDetection { - agentName: string; + agentId: string; command?: IChatAgentCommand; detected?: boolean; kind: 'agentDetection'; @@ -291,7 +292,7 @@ export interface IChatService { /** * Returns whether the request was accepted. */ - sendRequest(sessionId: string, message: string, implicitVariablesEnabled?: boolean, location?: ChatAgentLocation): Promise; + sendRequest(sessionId: string, message: string, implicitVariablesEnabled?: boolean, location?: ChatAgentLocation, parserContext?: IChatParserContext): Promise; removeRequest(sessionid: string, requestId: string): Promise; cancelCurrentRequestForSession(sessionId: string): void; clearSession(sessionId: string): void; diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index 7bc158070ba..f739e51a21b 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -25,7 +25,7 @@ import { ChatAgentLocation, IChatAgent, IChatAgentRequest, IChatAgentResult, ICh import { CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { ChatModel, ChatModelInitState, ChatRequestModel, ChatWelcomeMessageModel, IChatModel, IChatRequestVariableData, IChatRequestVariableEntry, ISerializableChatData, ISerializableChatsData, getHistoryEntriesFromModel, updateRanges } from 'vs/workbench/contrib/chat/common/chatModel'; import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, IParsedChatRequest, getPromptText } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; +import { ChatRequestParser, IChatParserContext } from 'vs/workbench/contrib/chat/common/chatRequestParser'; import { ChatCopyKind, IChat, IChatCompleteResponse, IChatDetail, IChatFollowup, IChatProgress, IChatProvider, IChatProviderInfo, IChatSendRequestData, IChatService, IChatTransferredSessionData, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; @@ -435,7 +435,7 @@ export class ChatService extends Disposable implements IChatService { return this._startSession(data.providerId, data, CancellationToken.None); } - async sendRequest(sessionId: string, request: string, implicitVariablesEnabled?: boolean, location: ChatAgentLocation = ChatAgentLocation.Panel): Promise { + async sendRequest(sessionId: string, request: string, implicitVariablesEnabled?: boolean, location: ChatAgentLocation = ChatAgentLocation.Panel, parserContext?: IChatParserContext): Promise { this.trace('sendRequest', `sessionId: ${sessionId}, message: ${request.substring(0, 20)}${request.length > 20 ? '[...]' : ''}}`); if (!request.trim()) { this.trace('sendRequest', 'Rejected empty message'); @@ -463,7 +463,7 @@ export class ChatService extends Disposable implements IChatService { defaultAgent = this.chatAgentService.getActivatedAgents().find(a => a.locations.includes(location)) ?? defaultAgent; } - const parsedRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionId, request, location); + const parsedRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionId, request, location, parserContext); const agent = parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart)?.agent ?? defaultAgent; const agentSlashCommandPart = parsedRequest.parts.find((r): r is ChatRequestAgentSubcommandPart => r instanceof ChatRequestAgentSubcommandPart); diff --git a/src/vs/workbench/contrib/chat/test/browser/chatVariables.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatVariables.test.ts index 35173312a8c..6d6856156ee 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatVariables.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatVariables.test.ts @@ -33,7 +33,7 @@ suite('ChatVariables', function () { instantiationService.stub(IExtensionService, new TestExtensionService()); instantiationService.stub(IChatVariablesService, service); instantiationService.stub(IChatService, new MockChatService()); - instantiationService.stub(IChatAgentService, testDisposables.add(instantiationService.createInstance(ChatAgentService))); + instantiationService.stub(IChatAgentService, instantiationService.createInstance(ChatAgentService)); }); test('ChatVariables - resolveVariables', async function () { diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_after_newline.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_after_newline.0.snap index e2255d23467..b37c012a2bf 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_after_newline.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_after_newline.0.snap @@ -27,6 +27,12 @@ }, agent: { id: "agent", + name: "agent", + extensionId: { + value: "nullExtensionDescription", + _lower: "nullextensiondescription" + }, + locations: [ ], metadata: { description: "" }, slashCommands: [ { diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_with_leading_whitespace.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_with_leading_whitespace.0.snap index 299d20d7ca8..40b03966b9d 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_with_leading_whitespace.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_with_leading_whitespace.0.snap @@ -27,6 +27,12 @@ }, agent: { id: "agent", + name: "agent", + extensionId: { + value: "nullExtensionDescription", + _lower: "nullextensiondescription" + }, + locations: [ ], metadata: { description: "" }, slashCommands: [ { diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap index 65c17a529ff..b1e969e3d1f 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap @@ -13,6 +13,12 @@ }, agent: { id: "agent", + name: "agent", + extensionId: { + value: "nullExtensionDescription", + _lower: "nullextensiondescription" + }, + locations: [ ], metadata: { description: "" }, slashCommands: [ { diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_subcommand_after_text.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_subcommand_after_text.0.snap index aa8eb44971e..231ac76a998 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_subcommand_after_text.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_subcommand_after_text.0.snap @@ -13,6 +13,12 @@ }, agent: { id: "agent", + name: "agent", + extensionId: { + value: "nullExtensionDescription", + _lower: "nullextensiondescription" + }, + locations: [ ], metadata: { description: "" }, slashCommands: [ { diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents__subCommand.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents__subCommand.0.snap index b53b074f918..d838e7852e5 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents__subCommand.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents__subCommand.0.snap @@ -13,6 +13,12 @@ }, agent: { id: "agent", + name: "agent", + extensionId: { + value: "nullExtensionDescription", + _lower: "nullextensiondescription" + }, + locations: [ ], metadata: { description: "" }, slashCommands: [ { diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline.0.snap index 93af3dc617c..e9cda39577c 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline.0.snap @@ -13,6 +13,12 @@ }, agent: { id: "agent", + name: "agent", + extensionId: { + value: "nullExtensionDescription", + _lower: "nullextensiondescription" + }, + locations: [ ], metadata: { description: "" }, slashCommands: [ { diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline__part2.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline__part2.0.snap index 92a8e9f426f..34ad195cc26 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline__part2.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline__part2.0.snap @@ -13,6 +13,12 @@ }, agent: { id: "agent", + name: "agent", + extensionId: { + value: "nullExtensionDescription", + _lower: "nullextensiondescription" + }, + locations: [ ], metadata: { description: "" }, slashCommands: [ { diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap new file mode 100644 index 00000000000..f474e2a2c47 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap @@ -0,0 +1,96 @@ +{ + requesterUsername: "test", + requesterAvatarIconUri: undefined, + responderUsername: "test", + responderAvatarIconUri: undefined, + welcomeMessage: undefined, + requests: [ + { + message: { + text: "@ChatProviderWithUsedContext test request", + parts: [ + { + kind: "agent", + range: { + start: 0, + endExclusive: 28 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 29 + }, + agent: { + id: "ChatProviderWithUsedContext", + name: "ChatProviderWithUsedContext", + description: undefined, + metadata: { } + } + }, + { + range: { + start: 28, + endExclusive: 41 + }, + editorRange: { + startLineNumber: 1, + startColumn: 29, + endLineNumber: 1, + endColumn: 42 + }, + text: " test request", + kind: "text" + } + ] + }, + variableData: { variables: [ ] }, + response: [ ], + result: { metadata: { metadataKey: "value" } }, + followups: undefined, + isCanceled: false, + vote: undefined, + agent: { + id: "ChatProviderWithUsedContext", + name: "ChatProviderWithUsedContext", + description: undefined, + extensionId: { + value: "nullExtensionDescription", + _lower: "nullextensiondescription" + }, + metadata: { }, + slashCommands: [ ], + locations: [ 1 ], + isDefault: undefined + }, + slashCommand: undefined, + usedContext: { + documents: [ + { + uri: { + scheme: "file", + authority: "", + path: "/test/path/to/file", + query: "", + fragment: "", + _formatted: null, + _fsPath: null + }, + version: 3, + ranges: [ + { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 2, + endColumn: 2 + } + ] + } + ], + kind: "usedContext" + }, + contentReferences: [ ] + } + ], + providerId: "testProvider" +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.0.snap new file mode 100644 index 00000000000..75c5fa71f40 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.0.snap @@ -0,0 +1,9 @@ +{ + requesterUsername: "test", + requesterAvatarIconUri: undefined, + responderUsername: "test", + responderAvatarIconUri: undefined, + welcomeMessage: undefined, + requests: [ ], + providerId: "testProvider" +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap new file mode 100644 index 00000000000..5a6706f49aa --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap @@ -0,0 +1,102 @@ +{ + requesterUsername: "test", + requesterAvatarIconUri: undefined, + responderUsername: "test", + responderAvatarIconUri: undefined, + welcomeMessage: undefined, + requests: [ + { + message: { + parts: [ + { + kind: "agent", + range: { + start: 0, + endExclusive: 28 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 29 + }, + agent: { + id: "ChatProviderWithUsedContext", + name: "ChatProviderWithUsedContext", + description: undefined, + metadata: { + requester: { name: "test" }, + fullName: "test" + } + } + }, + { + range: { + start: 28, + endExclusive: 41 + }, + editorRange: { + startLineNumber: 1, + startColumn: 29, + endLineNumber: 1, + endColumn: 42 + }, + text: " test request", + kind: "text" + } + ], + text: "@ChatProviderWithUsedContext test request" + }, + variableData: { variables: [ ] }, + response: [ ], + result: { metadata: { metadataKey: "value" } }, + followups: undefined, + isCanceled: false, + vote: undefined, + agent: { + id: "ChatProviderWithUsedContext", + name: "ChatProviderWithUsedContext", + description: undefined, + extensionId: { + value: "nullExtensionDescription", + _lower: "nullextensiondescription" + }, + metadata: { + requester: { name: "test" }, + fullName: "test" + }, + slashCommands: [ ], + locations: [ 1 ], + isDefault: undefined + }, + slashCommand: undefined, + usedContext: { + documents: [ + { + uri: { + scheme: "file", + authority: "", + path: "/test/path/to/file", + query: "", + fragment: "", + _formatted: null, + _fsPath: null + }, + version: 3, + ranges: [ + { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 2, + endColumn: 2 + } + ] + } + ], + kind: "usedContext" + }, + contentReferences: [ ] + } + ], + providerId: "testProvider" +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts b/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts index ffcda81f6de..3d63e2ed9d3 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts @@ -30,7 +30,7 @@ suite('ChatModel', () => { instantiationService.stub(IStorageService, testDisposables.add(new TestStorageService())); instantiationService.stub(ILogService, new NullLogService()); instantiationService.stub(IExtensionService, new TestExtensionService()); - instantiationService.stub(IChatAgentService, testDisposables.add(instantiationService.createInstance(ChatAgentService))); + instantiationService.stub(IChatAgentService, instantiationService.createInstance(ChatAgentService)); }); test('Waits for initialization', async () => { diff --git a/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts b/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts index c60a05eff5b..b94598b0afc 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts @@ -15,7 +15,7 @@ import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import { MockChatService } from 'vs/workbench/contrib/chat/test/common/mockChatService'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IExtensionService, nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { TestExtensionService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; suite('ChatRequestParser', () => { @@ -31,7 +31,7 @@ suite('ChatRequestParser', () => { instantiationService.stub(ILogService, new NullLogService()); instantiationService.stub(IExtensionService, new TestExtensionService()); instantiationService.stub(IChatService, new MockChatService()); - instantiationService.stub(IChatAgentService, testDisposables.add(instantiationService.createInstance(ChatAgentService))); + instantiationService.stub(IChatAgentService, instantiationService.createInstance(ChatAgentService)); varService = mockObject()({}); varService.getDynamicVariables.returns([]); @@ -112,12 +112,12 @@ suite('ChatRequestParser', () => { }); const getAgentWithSlashCommands = (slashCommands: IChatAgentCommand[]) => { - return { id: 'agent', metadata: { description: '' }, slashCommands, locations: [ChatAgentLocation.Panel] }; + return { id: 'agent', name: 'agent', extensionId: nullExtensionDescription.identifier, locations: [], metadata: { description: '' }, slashCommands, locations: [ChatAgentLocation.Panel] }; }; test('agent with subcommand after text', async () => { const agentsService = mockObject()({}); - agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); + agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]); instantiationService.stub(IChatAgentService, agentsService as any); parser = instantiationService.createInstance(ChatRequestParser); @@ -127,7 +127,7 @@ suite('ChatRequestParser', () => { test('agents, subCommand', async () => { const agentsService = mockObject()({}); - agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); + agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]); instantiationService.stub(IChatAgentService, agentsService as any); parser = instantiationService.createInstance(ChatRequestParser); @@ -137,7 +137,7 @@ suite('ChatRequestParser', () => { test('agent with question mark', async () => { const agentsService = mockObject()({}); - agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); + agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]); instantiationService.stub(IChatAgentService, agentsService as any); parser = instantiationService.createInstance(ChatRequestParser); @@ -147,7 +147,7 @@ suite('ChatRequestParser', () => { test('agent and subcommand with leading whitespace', async () => { const agentsService = mockObject()({}); - agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); + agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]); instantiationService.stub(IChatAgentService, agentsService as any); parser = instantiationService.createInstance(ChatRequestParser); @@ -157,7 +157,7 @@ suite('ChatRequestParser', () => { test('agent and subcommand after newline', async () => { const agentsService = mockObject()({}); - agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); + agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]); instantiationService.stub(IChatAgentService, agentsService as any); parser = instantiationService.createInstance(ChatRequestParser); @@ -167,7 +167,7 @@ suite('ChatRequestParser', () => { test('agent not first', async () => { const agentsService = mockObject()({}); - agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); + agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]); instantiationService.stub(IChatAgentService, agentsService as any); parser = instantiationService.createInstance(ChatRequestParser); @@ -177,7 +177,7 @@ suite('ChatRequestParser', () => { test('agents and variables and multiline', async () => { const agentsService = mockObject()({}); - agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); + agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]); instantiationService.stub(IChatAgentService, agentsService as any); varService.hasVariable.returns(true); @@ -189,7 +189,7 @@ suite('ChatRequestParser', () => { test('agents and variables and multiline, part2', async () => { const agentsService = mockObject()({}); - agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); + agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]); instantiationService.stub(IChatAgentService, agentsService as any); varService.hasVariable.returns(true); diff --git a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts index 64111ef5f30..3125cf09d88 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts @@ -20,7 +20,6 @@ import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; import { ChatAgentLocation, ChatAgentService, IChatAgent, IChatAgentImplementation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; import { ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel'; @@ -28,11 +27,11 @@ import { IChat, IChatFollowup, IChatProgress, IChatProvider, IChatRequest, IChat import { ChatService } from 'vs/workbench/contrib/chat/common/chatServiceImpl'; import { ChatSlashCommandService, IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; +import { MockChatService } from 'vs/workbench/contrib/chat/test/common/mockChatService'; import { MockChatVariablesService } from 'vs/workbench/contrib/chat/test/common/mockChatVariables'; import { IExtensionService, nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; +import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; import { TestContextService, TestExtensionService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; -import { MockChatService } from 'vs/workbench/contrib/chat/test/common/mockChatService'; -import { MockChatContributionService } from 'vs/workbench/contrib/chat/test/common/mockChatContributionService'; class SimpleTestProvider extends Disposable implements IChatProvider { private static sessionId = 0; @@ -57,6 +56,7 @@ class SimpleTestProvider extends Disposable implements IChatProvider { const chatAgentWithUsedContextId = 'ChatProviderWithUsedContext'; const chatAgentWithUsedContext: IChatAgent = { id: chatAgentWithUsedContextId, + name: chatAgentWithUsedContextId, extensionId: nullExtensionDescription.identifier, locations: [ChatAgentLocation.Panel], metadata: {}, @@ -82,7 +82,7 @@ const chatAgentWithUsedContext: IChatAgent = { }, }; -suite('Chat', () => { +suite('ChatService', () => { const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); let storageService: IStorageService; @@ -104,13 +104,8 @@ suite('Chat', () => { instantiationService.stub(IWorkspaceContextService, new TestContextService()); instantiationService.stub(IChatSlashCommandService, testDisposables.add(instantiationService.createInstance(ChatSlashCommandService))); instantiationService.stub(IChatService, new MockChatService()); - instantiationService.stub(IChatContributionService, new MockChatContributionService( - [ - { extensionId: nullExtensionDescription.identifier, name: 'testAgent', isDefault: true }, - { extensionId: nullExtensionDescription.identifier, name: chatAgentWithUsedContextId }, - ])); - chatAgentService = testDisposables.add(instantiationService.createInstance(ChatAgentService)); + chatAgentService = instantiationService.createInstance(ChatAgentService); instantiationService.stub(IChatAgentService, chatAgentService); const agent = { @@ -118,7 +113,9 @@ suite('Chat', () => { return {}; }, } satisfies IChatAgentImplementation; - testDisposables.add(chatAgentService.registerAgent('testAgent', agent)); + testDisposables.add(chatAgentService.registerAgent('testAgent', { name: 'testAgent', id: 'testAgent', isDefault: true, extensionId: nullExtensionDescription.identifier, locations: [ChatAgentLocation.Panel], metadata: {}, slashCommands: [] })); + testDisposables.add(chatAgentService.registerAgent(chatAgentWithUsedContextId, { name: chatAgentWithUsedContextId, id: chatAgentWithUsedContextId, extensionId: nullExtensionDescription.identifier, locations: [ChatAgentLocation.Panel], metadata: {}, slashCommands: [] })); + testDisposables.add(chatAgentService.registerAgentImplementation('testAgent', agent)); chatAgentService.updateAgent('testAgent', { requester: { name: 'test' }, fullName: 'test' }); }); @@ -209,7 +206,7 @@ suite('Chat', () => { }); test('can serialize', async () => { - testDisposables.add(chatAgentService.registerAgent(chatAgentWithUsedContext.id, chatAgentWithUsedContext)); + testDisposables.add(chatAgentService.registerAgentImplementation(chatAgentWithUsedContextId, chatAgentWithUsedContext)); chatAgentService.updateAgent(chatAgentWithUsedContextId, { requester: { name: 'test' }, fullName: 'test' }); const testService = testDisposables.add(instantiationService.createInstance(ChatService)); testDisposables.add(testService.registerProvider(testDisposables.add(new SimpleTestProvider('testProvider')))); @@ -230,7 +227,7 @@ suite('Chat', () => { test('can deserialize', async () => { let serializedChatData: ISerializableChatData; - testDisposables.add(chatAgentService.registerAgent(chatAgentWithUsedContext.id, chatAgentWithUsedContext)); + testDisposables.add(chatAgentService.registerAgentImplementation(chatAgentWithUsedContextId, chatAgentWithUsedContext)); // create the first service, send request, get response, and serialize the state { // serapate block to not leak variables in outer scope diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatContributionService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatContributionService.ts index e1adddb2dec..27a687cb24d 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatContributionService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatContributionService.ts @@ -9,7 +9,6 @@ export class MockChatContributionService implements IChatContributionService { _serviceBrand: undefined; constructor( - public readonly registeredParticipants: IChatParticipantContribution[] = [] ) { } registeredProviders: IChatProviderContribution[] = []; diff --git a/src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts b/src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts index c993bae97ea..97e11f717ea 100644 --- a/src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts @@ -28,7 +28,10 @@ suite('VoiceChat', () => { extensionId: ExtensionIdentifier = nullExtensionDescription.identifier; locations: ChatAgentLocation[] = [ChatAgentLocation.Panel]; - constructor(readonly id: string, readonly slashCommands: IChatAgentCommand[]) { } + public readonly name: string; + constructor(readonly id: string, readonly slashCommands: IChatAgentCommand[]) { + this.name = id; + } invoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { throw new Error('Method not implemented.'); } provideWelcomeMessage?(token: CancellationToken): ProviderResult<(string | IMarkdownString)[] | undefined> { throw new Error('Method not implemented.'); } metadata = {}; @@ -47,17 +50,18 @@ suite('VoiceChat', () => { class TestChatAgentService implements IChatAgentService { _serviceBrand: undefined; readonly onDidChangeAgents = Event.None; - registerAgent(name: string, agent: IChatAgentImplementation): IDisposable { throw new Error(); } + registerAgentImplementation(id: string, agent: IChatAgentImplementation): IDisposable { throw new Error(); } registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable { throw new Error('Method not implemented.'); } invokeAgent(id: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { throw new Error(); } getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { throw new Error(); } - getRegisteredAgents(): Array { return agents; } getActivatedAgents(): IChatAgent[] { return agents; } getAgents(): IChatAgent[] { return agents; } - getAgent(id: string): IChatAgent | undefined { throw new Error(); } getDefaultAgent(): IChatAgent | undefined { throw new Error(); } getSecondaryAgent(): IChatAgent | undefined { throw new Error(); } - updateAgent(id: string, updateMetadata: IChatAgentMetadata): void { throw new Error(); } + registerAgent(id: string, data: IChatAgentData): IDisposable { throw new Error('Method not implemented.'); } + getAgent(id: string): IChatAgentData | undefined { throw new Error('Method not implemented.'); } + getAgentsByName(name: string): IChatAgentData[] { throw new Error('Method not implemented.'); } + updateAgent(id: string, updateMetadata: IChatAgentMetadata): void { throw new Error('Method not implemented.'); } } class TestSpeechService implements ISpeechService { diff --git a/src/vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint.ts b/src/vs/workbench/contrib/codeEditor/common/languageConfigurationExtensionPoint.ts similarity index 95% rename from src/vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint.ts rename to src/vs/workbench/contrib/codeEditor/common/languageConfigurationExtensionPoint.ts index f602d051103..e3d1fcd1064 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint.ts +++ b/src/vs/workbench/contrib/codeEditor/common/languageConfigurationExtensionPoint.ts @@ -47,7 +47,7 @@ interface IOnEnterRule { /** * Serialized form of a language configuration */ -interface ILanguageConfiguration { +export interface ILanguageConfiguration { comments?: CommentRule; brackets?: CharacterPair[]; autoClosingPairs?: Array; @@ -149,7 +149,7 @@ export class LanguageConfigurationFileHandler extends Disposable { } } - private _extractValidCommentRule(languageId: string, configuration: ILanguageConfiguration): CommentRule | undefined { + private static _extractValidCommentRule(languageId: string, configuration: ILanguageConfiguration): CommentRule | undefined { const source = configuration.comments; if (typeof source === 'undefined') { return undefined; @@ -179,7 +179,7 @@ export class LanguageConfigurationFileHandler extends Disposable { return result; } - private _extractValidBrackets(languageId: string, configuration: ILanguageConfiguration): CharacterPair[] | undefined { + private static _extractValidBrackets(languageId: string, configuration: ILanguageConfiguration): CharacterPair[] | undefined { const source = configuration.brackets; if (typeof source === 'undefined') { return undefined; @@ -203,7 +203,7 @@ export class LanguageConfigurationFileHandler extends Disposable { return result; } - private _extractValidAutoClosingPairs(languageId: string, configuration: ILanguageConfiguration): IAutoClosingPairConditional[] | undefined { + private static _extractValidAutoClosingPairs(languageId: string, configuration: ILanguageConfiguration): IAutoClosingPairConditional[] | undefined { const source = configuration.autoClosingPairs; if (typeof source === 'undefined') { return undefined; @@ -249,7 +249,7 @@ export class LanguageConfigurationFileHandler extends Disposable { return result; } - private _extractValidSurroundingPairs(languageId: string, configuration: ILanguageConfiguration): IAutoClosingPair[] | undefined { + private static _extractValidSurroundingPairs(languageId: string, configuration: ILanguageConfiguration): IAutoClosingPair[] | undefined { const source = configuration.surroundingPairs; if (typeof source === 'undefined') { return undefined; @@ -289,7 +289,7 @@ export class LanguageConfigurationFileHandler extends Disposable { return result; } - private _extractValidColorizedBracketPairs(languageId: string, configuration: ILanguageConfiguration): CharacterPair[] | undefined { + private static _extractValidColorizedBracketPairs(languageId: string, configuration: ILanguageConfiguration): CharacterPair[] | undefined { const source = configuration.colorizedBracketPairs; if (typeof source === 'undefined') { return undefined; @@ -312,7 +312,7 @@ export class LanguageConfigurationFileHandler extends Disposable { return result; } - private _extractValidOnEnterRules(languageId: string, configuration: ILanguageConfiguration): OnEnterRule[] | undefined { + private static _extractValidOnEnterRules(languageId: string, configuration: ILanguageConfiguration): OnEnterRule[] | undefined { const source = configuration.onEnterRules; if (typeof source === 'undefined') { return undefined; @@ -385,7 +385,7 @@ export class LanguageConfigurationFileHandler extends Disposable { return result; } - private _handleConfig(languageId: string, configuration: ILanguageConfiguration): void { + public static extractValidConfig(languageId: string, configuration: ILanguageConfiguration): ExplicitLanguageConfiguration { const comments = this._extractValidCommentRule(languageId, configuration); const brackets = this._extractValidBrackets(languageId, configuration); @@ -421,11 +421,15 @@ export class LanguageConfigurationFileHandler extends Disposable { folding, __electricCharacterSupport: undefined, }; + return richEditConfig; + } + private _handleConfig(languageId: string, configuration: ILanguageConfiguration): void { + const richEditConfig = LanguageConfigurationFileHandler.extractValidConfig(languageId, configuration); this._languageConfigurationService.register(languageId, richEditConfig, 50); } - private _parseRegex(languageId: string, confPath: string, value: string | IRegExp): RegExp | undefined { + private static _parseRegex(languageId: string, confPath: string, value: string | IRegExp): RegExp | undefined { if (typeof value === 'string') { try { return new RegExp(value, ''); @@ -454,7 +458,7 @@ export class LanguageConfigurationFileHandler extends Disposable { return undefined; } - private _mapIndentationRules(languageId: string, indentationRules: IIndentationRules): IndentationRule | undefined { + private static _mapIndentationRules(languageId: string, indentationRules: IIndentationRules): IndentationRule | undefined { const increaseIndentPattern = this._parseRegex(languageId, `indentationRules.increaseIndentPattern`, indentationRules.increaseIndentPattern); if (!increaseIndentPattern) { return undefined; diff --git a/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts b/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts new file mode 100644 index 00000000000..8f08b401e60 --- /dev/null +++ b/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts @@ -0,0 +1,312 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as fs from 'fs'; +import * as path from 'path'; +import * as assert from 'assert'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { getReindentEditOperations } from 'vs/editor/contrib/indentation/common/indentation'; +import { IRelaxedTextModelCreationOptions, createModelServices, instantiateTextModel } from 'vs/editor/test/common/testTextModel'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { ILanguageConfiguration, LanguageConfigurationFileHandler } from 'vs/workbench/contrib/codeEditor/common/languageConfigurationExtensionPoint'; +import { parse } from 'vs/base/common/json'; +import { IRange } from 'vs/editor/common/core/range'; + +function getIRange(range: IRange): IRange { + return { + startLineNumber: range.startLineNumber, + startColumn: range.startColumn, + endLineNumber: range.endLineNumber, + endColumn: range.endColumn + }; +} + +suite('Auto-Reindentation - TypeScript/JavaScript', () => { + + const languageId = 'ts-test'; + const options: IRelaxedTextModelCreationOptions = {}; + let disposables: DisposableStore; + let instantiationService: TestInstantiationService; + let languageConfigurationService: ILanguageConfigurationService; + + setup(() => { + disposables = new DisposableStore(); + instantiationService = createModelServices(disposables); + languageConfigurationService = instantiationService.get(ILanguageConfigurationService); + const configPath = path.join('extensions', 'typescript-basics', 'language-configuration.json'); + const configString = fs.readFileSync(configPath).toString(); + const config = parse(configString, []); + const configParsed = LanguageConfigurationFileHandler.extractValidConfig(languageId, config); + disposables.add(languageConfigurationService.register(languageId, configParsed)); + }); + + teardown(() => { + disposables.dispose(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + // Test which can be ran to find cases of incorrect indentation... + test.skip('Find Cases of Incorrect Indentation', () => { + + const filePath = path.join('..', 'TypeScript', 'src', 'server', 'utilities.ts'); + const fileContents = fs.readFileSync(filePath).toString(); + + const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); + const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); + model.applyEdits(editOperations); + + // save the files to disk + const initialFile = path.join('..', 'autoindent', 'initial.ts'); + const finalFile = path.join('..', 'autoindent', 'final.ts'); + fs.writeFileSync(initialFile, fileContents); + fs.writeFileSync(finalFile, model.getValue()); + }); + + // Unit tests for increase and decrease indent patterns... + + /** + * First increase indent and decrease indent patterns: + * + * - decreaseIndentPattern: /^(.*\*\/)?\s*\}.*$/ + * - In (https://macromates.com/manual/en/appendix) + * Either we have white space before the closing bracket, or we have a multi line comment ending on that line followed by whitespaces + * This is followed by any character. + * Textmate decrease indent pattern is as follows: /^(.*\*\/)?\s*\}[;\s]*$/ + * Presumably allowing multi line comments ending on that line implies that } is itself not part of a multi line comment + * + * - increaseIndentPattern: /^.*\{[^}"']*$/ + * - In (https://macromates.com/manual/en/appendix) + * This regex means that we increase the indent when we have any characters followed by the opening brace, followed by characters + * except for closing brace }, double quotes " or single quote '. + * The } is checked in order to avoid the indentation in the following case `int arr[] = { 1, 2, 3 };` + * The double quote and single quote are checked in order to avoid the indentation in the following case: str = "foo {"; + */ + + test('Issue #25437', () => { + // issue: https://github.com/microsoft/vscode/issues/25437 + // fix: https://github.com/microsoft/vscode/commit/8c82a6c6158574e098561c28d470711f1b484fc8 + // explanation: var foo = `{`; should not increase indentation + + // increaseIndentPattern: /^.*\{[^}"']*$/ -> /^.*\{[^}"'`]*$/ + + const fileContents = [ + 'const foo = `{`;', + ' ', + ].join('\n'); + const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); + const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); + assert.deepStrictEqual(editOperations.length, 1); + const operation = editOperations[0]; + assert.deepStrictEqual(getIRange(operation.range), { + "startLineNumber": 2, + "startColumn": 1, + "endLineNumber": 2, + "endColumn": 5, + }); + assert.deepStrictEqual(operation.text, ''); + }); + + test('Enriching the hover', () => { + // issue: - + // fix: https://github.com/microsoft/vscode/commit/19ae0932c45b1096443a8c1335cf1e02eb99e16d + // explanation: + // - decrease indent on ) and ] also + // - increase indent on ( and [ also + + // decreaseIndentPattern: /^(.*\*\/)?\s*\}.*$/ -> /^(.*\*\/)?\s*[\}\]\)].*$/ + // increaseIndentPattern: /^.*\{[^}"'`]*$/ -> /^.*(\{[^}"'`]*|\([^)"'`]*|\[[^\]"'`]*)$/ + + let fileContents = [ + 'function foo(', + ' bar: string', + ' ){}', + ].join('\n'); + let model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); + let editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); + assert.deepStrictEqual(editOperations.length, 1); + let operation = editOperations[0]; + assert.deepStrictEqual(getIRange(operation.range), { + "startLineNumber": 3, + "startColumn": 1, + "endLineNumber": 3, + "endColumn": 5, + }); + assert.deepStrictEqual(operation.text, ''); + + fileContents = [ + 'function foo(', + 'bar: string', + '){}', + ].join('\n'); + model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); + editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); + assert.deepStrictEqual(editOperations.length, 1); + operation = editOperations[0]; + assert.deepStrictEqual(getIRange(operation.range), { + "startLineNumber": 2, + "startColumn": 1, + "endLineNumber": 2, + "endColumn": 1, + }); + assert.deepStrictEqual(operation.text, ' '); + }); + + test('Issue #86176', () => { + // issue: https://github.com/microsoft/vscode/issues/86176 + // fix: https://github.com/microsoft/vscode/commit/d89e2e17a5d1ba37c99b1d3929eb6180a5bfc7a8 + // explanation: When quotation marks are present on the first line of an if statement or for loop, following line should not be indented + + // increaseIndentPattern: /^((?!\/\/).)*(\{[^}"'`]*|\([^)"'`]*|\[[^\]"'`]*)$/ -> /^((?!\/\/).)*(\{([^}"'`]*|(\t|[ ])*\/\/.*)|\([^)"'`]*|\[[^\]"'`]*)$/ + // explanation: after open brace, do not decrease indent if it is followed on the same line by " // " + // todo@aiday-mar: should also apply for when it follows ( and [ + + const fileContents = [ + `if () { // '`, + `x = 4`, + `}` + ].join('\n'); + const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); + const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); + assert.deepStrictEqual(editOperations.length, 1); + const operation = editOperations[0]; + assert.deepStrictEqual(getIRange(operation.range), { + "startLineNumber": 2, + "startColumn": 1, + "endLineNumber": 2, + "endColumn": 1, + }); + assert.deepStrictEqual(operation.text, ' '); + }); + + test('Issue #141816', () => { + + // issue: https://github.com/microsoft/vscode/issues/141816 + // fix: https://github.com/microsoft/vscode/pull/141997/files + // explanation: if (, [, {, is followed by a forward slash then assume we are in a regex pattern, and do not indent + + // increaseIndentPattern: /^((?!\/\/).)*(\{([^}"'`]*|(\t|[ ])*\/\/.*)|\([^)"'`]*|\[[^\]"'`]*)$/ -> /^((?!\/\/).)*(\{([^}"'`/]*|(\t|[ ])*\/\/.*)|\([^)"'`/]*|\[[^\]"'`/]*)$/ + // -> Final current increase indent pattern + + const fileContents = [ + 'const r = /{/;', + ' ', + ].join('\n'); + const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); + const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); + assert.deepStrictEqual(editOperations.length, 1); + const operation = editOperations[0]; + assert.deepStrictEqual(getIRange(operation.range), { + "startLineNumber": 2, + "startColumn": 1, + "endLineNumber": 2, + "endColumn": 4, + }); + assert.deepStrictEqual(operation.text, ''); + }); + + test('Issue #29886', () => { + // issue: https://github.com/microsoft/vscode/issues/29886 + // fix: https://github.com/microsoft/vscode/commit/7910b3d7bab8a721aae98dc05af0b5e1ea9d9782 + + // decreaseIndentPattern: /^(.*\*\/)?\s*[\}\]\)].*$/ -> /^((?!.*?\/\*).*\*\/)?\s*[\}\]\)].*$/ + // -> Final current decrease indent pattern + + // explanation: Positive lookahead: (?= «pattern») matches if pattern matches what comes after the current location in the input string. + // Negative lookahead: (?! «pattern») matches if pattern does not match what comes after the current location in the input string + // The change proposed is to not decrease the indent if there is a multi-line comment ending on the same line before the closing parentheses + + const fileContents = [ + 'function foo() {', + ' bar(/* */)', + '};', + ].join('\n'); + const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); + const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); + assert.deepStrictEqual(editOperations.length, 0); + }); + + // Failing tests inferred from the current regexes... + + test.skip('Incorrect deindentation after `*/}` string', () => { + + // explanation: If */ was not before the }, the regex does not allow characters before the }, so there would not be an indent + // Here since there is */ before the }, the regex allows all the characters before, hence there is a deindent + + const fileContents = [ + `const obj = {`, + ` obj1: {`, + ` brace : '*/}'`, + ` }`, + `}`, + ].join('\n'); + const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); + const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); + assert.deepStrictEqual(editOperations.length, 0); + }); + + // Failing tests from issues... + + test.skip('Issue #56275', () => { + + // issue: https://github.com/microsoft/vscode/issues/56275 + // explanation: If */ was not before the }, the regex does not allow characters before the }, so there would not be an indent + // Here since there is */ before the }, the regex allows all the characters before, hence there is a deindent + + let fileContents = [ + 'function foo() {', + ' var bar = (/b*/);', + '}', + ].join('\n'); + let model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); + let editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); + assert.deepStrictEqual(editOperations.length, 0); + + fileContents = [ + 'function foo() {', + ' var bar = "/b*/)";', + '}', + ].join('\n'); + model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); + editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); + assert.deepStrictEqual(editOperations.length, 0); + }); + + test.skip('Issue #116843', () => { + + // issue: https://github.com/microsoft/vscode/issues/116843 + // related: https://github.com/microsoft/vscode/issues/43244 + // explanation: When you have an arrow function, you don't have { or }, but you would expect indentation to still be done in that way + + /* + Notes: Currently the reindent edit operations does not call the onEnter rules + The reindent should also call the onEnter rules to get the correct indentation + */ + const fileContents = [ + 'const add1 = (n) =>', + ' n + 1;', + ].join('\n'); + const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); + const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); + assert.deepStrictEqual(editOperations.length, 0); + }); + + test.skip('Issue #185252', () => { + + // issue: https://github.com/microsoft/vscode/issues/185252 + // explanation: Reindenting the comment correctly + + const fileContents = [ + '/*', + ' * This is a comment.', + ' */', + ].join('\n'); + const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); + const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); + assert.deepStrictEqual(editOperations.length, 0); + }); +}); diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts index a42202d6550..bc7c5bfc8b4 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts @@ -8,13 +8,17 @@ import { equals } from 'vs/base/common/arrays'; import { timeout } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; import { mock } from 'vs/base/test/common/mock'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; import { IDiffProviderFactoryService } from 'vs/editor/browser/widget/diffEditor/diffProviderFactoryService'; +import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Range } from 'vs/editor/common/core/range'; import { ITextModel } from 'vs/editor/common/model'; +import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { IModelService } from 'vs/editor/common/services/model'; +import { TestDiffProviderFactoryService } from 'vs/editor/test/browser/diff/testDiffProviderFactoryService'; import { instantiateTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; @@ -28,23 +32,19 @@ import { IViewDescriptorService } from 'vs/workbench/common/views'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; import { IChatAccessibilityService, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; +import { ChatAgentService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { InlineChatController, InlineChatRunOptions, State } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; -import { IInlineChatSavingService } from '../../browser/inlineChatSavingService'; import { Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; -import { InlineChatSessionServiceImpl } from '../../browser/inlineChatSessionServiceImpl'; -import { IInlineChatSessionService } from '../../browser/inlineChatSessionService'; import { CTX_INLINE_CHAT_USER_DID_EDIT, EditMode, IInlineChatRequest, IInlineChatService, InlineChatConfigKeys, InlineChatResponseType } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { InlineChatServiceImpl } from 'vs/workbench/contrib/inlineChat/common/inlineChatServiceImpl'; import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; -import { EditOperation } from 'vs/editor/common/core/editOperation'; +import { IInlineChatSavingService } from '../../browser/inlineChatSavingService'; +import { IInlineChatSessionService } from '../../browser/inlineChatSessionService'; +import { InlineChatSessionServiceImpl } from '../../browser/inlineChatSessionServiceImpl'; import { TestWorkerService } from './testWorkerService'; -import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; -import { Schemas } from 'vs/base/common/network'; import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; -import { MockChatContributionService } from 'vs/workbench/contrib/chat/test/common/mockChatContributionService'; import { IExtensionService, nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; -import { ChatAgentService, IChatAgentImplementation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { ChatService } from 'vs/workbench/contrib/chat/common/chatServiceImpl'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; @@ -56,7 +56,6 @@ import { TestContextService, TestExtensionService } from 'vs/workbench/test/comm import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; import { ChatSlashCommandService, IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; -import { TestDiffProviderFactoryService } from 'vs/editor/test/browser/diff/testDiffProviderFactoryService'; import { ChatWidgetService } from 'vs/workbench/contrib/chat/browser/chatWidget'; import { ChatWidgetHistoryService, IChatWidgetHistoryService } from 'vs/workbench/contrib/chat/common/chatWidgetHistoryService'; @@ -133,8 +132,6 @@ suite('InteractiveChatController', function () { [IChatService, new SyncDescriptor(ChatService)], [IEditorWorkerService, new SyncDescriptor(TestWorkerService)], [IContextKeyService, contextKeyService], - [IChatContributionService, new MockChatContributionService( - [{ extensionId: nullExtensionDescription.identifier, name: 'testAgent', isDefault: true }])], [IChatAgentService, new SyncDescriptor(ChatAgentService)], [IInlineChatService, new SyncDescriptor(InlineChatServiceImpl)], [IDiffProviderFactoryService, new SyncDescriptor(TestDiffProviderFactoryService)], @@ -168,7 +165,7 @@ suite('InteractiveChatController', function () { }] ); - instaService = store.add(workbenchInstantiationService(undefined, store).createChild(serviceCollection)); + instaService = store.add((store.add(workbenchInstantiationService(undefined, store))).createChild(serviceCollection)); configurationService = instaService.get(IConfigurationService) as TestConfigurationService; configurationService.setUserConfiguration('chat', { editor: { fontSize: 14, fontFamily: 'default' } }); @@ -186,7 +183,6 @@ suite('InteractiveChatController', function () { }, } satisfies IChatAgentImplementation; store.add(chatAgentService.registerAgent('testAgent', agent)); - inlineChatSessionService = store.add(instaService.get(IInlineChatSessionService)); model = store.add(instaService.get(IModelService).createModel('Hello\nWorld\nHello Again\nHello World\n', null)); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/cellCommands/cellCommands.ts b/src/vs/workbench/contrib/notebook/browser/contrib/cellCommands/cellCommands.ts index 77a91525426..2027ddaa5b7 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/cellCommands/cellCommands.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/cellCommands/cellCommands.ts @@ -14,14 +14,18 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits'; import { changeCellToKind, computeCellLinesContents, copyCellRange, joinCellsWithSurrounds, joinSelectedCells, moveCellRange } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations'; -import { cellExecutionArgs, CellOverflowToolbarGroups, CellToolbarOrder, CELL_TITLE_CELL_GROUP_ID, INotebookCellActionContext, INotebookCellToolbarActionContext, INotebookCommandContext, NotebookCellAction, NotebookMultiCellAction, parseMultiCellExecutionArgs } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; +import { cellExecutionArgs, CellOverflowToolbarGroups, CellToolbarOrder, CELL_TITLE_CELL_GROUP_ID, INotebookCellActionContext, INotebookCellToolbarActionContext, INotebookCommandContext, NotebookCellAction, NotebookMultiCellAction, parseMultiCellExecutionArgs, findTargetCellEditor } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; import { CellFocusMode, EXPAND_CELL_INPUT_COMMAND_ID, EXPAND_CELL_OUTPUT_COMMAND_ID, ICellOutputViewModel, ICellViewModel, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_EDITOR_FOCUSED, NOTEBOOK_CELL_FOCUSED, NOTEBOOK_CELL_HAS_ERROR_DIAGNOSTICS, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import * as icons from 'vs/workbench/contrib/notebook/browser/notebookIcons'; import { CellEditType, CellKind, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; +import { Range } from 'vs/editor/common/core/range'; +import { CodeActionController } from 'vs/editor/contrib/codeAction/browser/codeActionController'; +import { CodeActionKind, CodeActionTriggerSource } from 'vs/editor/contrib/codeAction/common/types'; //#region Move/Copy cells const MOVE_CELL_UP_COMMAND_ID = 'notebook.cell.moveUp'; @@ -353,6 +357,7 @@ const COLLAPSE_ALL_CELL_OUTPUTS_COMMAND_ID = 'notebook.cell.collapseAllCellOutpu const EXPAND_ALL_CELL_OUTPUTS_COMMAND_ID = 'notebook.cell.expandAllCellOutputs'; const TOGGLE_CELL_OUTPUTS_COMMAND_ID = 'notebook.cell.toggleOutputs'; const TOGGLE_CELL_OUTPUT_SCROLLING = 'notebook.cell.toggleOutputScrolling'; +const OPEN_CELL_FAILURE_ACTIONS_COMMAND_ID = 'notebook.cell.openFailureActions'; registerAction2(class CollapseCellInputAction extends NotebookMultiCellAction { constructor() { @@ -579,6 +584,45 @@ registerAction2(class ToggleCellOutputScrolling extends NotebookMultiCellAction } }); +registerAction2(class ExpandAllCellOutputsAction extends NotebookCellAction { + constructor() { + super({ + id: OPEN_CELL_FAILURE_ACTIONS_COMMAND_ID, + title: localize2('notebookActions.cellFailureActions', "Show Cell Failure Actions"), + precondition: ContextKeyExpr.and(NOTEBOOK_CELL_FOCUSED, NOTEBOOK_CELL_HAS_ERROR_DIAGNOSTICS, NOTEBOOK_CELL_EDITOR_FOCUSED.toNegated()), + f1: true, + keybinding: { + when: ContextKeyExpr.and(NOTEBOOK_CELL_FOCUSED, NOTEBOOK_CELL_HAS_ERROR_DIAGNOSTICS, NOTEBOOK_CELL_EDITOR_FOCUSED.toNegated()), + primary: KeyMod.CtrlCmd | KeyCode.Period, + weight: KeybindingWeight.WorkbenchContrib + } + }); + } + + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { + if (context.cell instanceof CodeCellViewModel) { + const error = context.cell.cellErrorDetails; + if (error?.location) { + const location = Range.lift({ + startLineNumber: error.location.startLineNumber + 1, + startColumn: error.location.startColumn + 1, + endLineNumber: error.location.endLineNumber + 1, + endColumn: error.location.endColumn + 1 + }); + context.notebookEditor.setCellEditorSelection(context.cell, Range.lift(location)); + const editor = findTargetCellEditor(context, context.cell); + if (editor) { + const controller = CodeActionController.get(editor); + controller?.manualTriggerAtCurrentPosition( + localize('cellCommands.quickFix.noneMessage', "No code actions available"), + CodeActionTriggerSource.Default, + { include: CodeActionKind.QuickFix }); + } + } + } + } +}); + //#endregion function forEachCell(editor: INotebookEditor, callback: (cell: ICellViewModel, index: number) => void) { diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnostics.ts b/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnostics.ts index 154a590812b..054e1fda1a4 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnostics.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnostics.ts @@ -70,7 +70,9 @@ export class CellDiagnostics extends Disposable { } public clear() { - this.clearDiagnostics(); + if (this.ErrorDetails) { + this.clearDiagnostics(); + } } private clearDiagnostics() { diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellContextKeys.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellContextKeys.ts index 7bc1ba28b3a..f3571422003 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellContextKeys.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellContextKeys.ts @@ -13,7 +13,7 @@ import { CellContentPart } from 'vs/workbench/contrib/notebook/browser/view/cell import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel'; import { NotebookCellExecutionState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { NotebookCellExecutionStateContext, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_EDITOR_FOCUSED, NOTEBOOK_CELL_EXECUTING, NOTEBOOK_CELL_EXECUTION_STATE, NOTEBOOK_CELL_FOCUSED, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_LINE_NUMBERS, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_RESOURCE, NOTEBOOK_CELL_TYPE, NOTEBOOK_CELL_GENERATED_BY_CHAT } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { NotebookCellExecutionStateContext, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_EDITOR_FOCUSED, NOTEBOOK_CELL_EXECUTING, NOTEBOOK_CELL_EXECUTION_STATE, NOTEBOOK_CELL_FOCUSED, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_LINE_NUMBERS, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_RESOURCE, NOTEBOOK_CELL_TYPE, NOTEBOOK_CELL_GENERATED_BY_CHAT, NOTEBOOK_CELL_HAS_ERROR_DIAGNOSTICS } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { INotebookExecutionStateService, NotebookExecutionType } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; export class CellContextKeyPart extends CellContentPart { @@ -47,6 +47,7 @@ export class CellContextKeyManager extends Disposable { private cellLineNumbers!: IContextKey<'on' | 'off' | 'inherit'>; private cellResource!: IContextKey; private cellGeneratedByChat!: IContextKey; + private cellHasErrorDiagnostics!: IContextKey; private markdownEditMode!: IContextKey; @@ -74,6 +75,7 @@ export class CellContextKeyManager extends Disposable { this.cellLineNumbers = NOTEBOOK_CELL_LINE_NUMBERS.bindTo(this._contextKeyService); this.cellGeneratedByChat = NOTEBOOK_CELL_GENERATED_BY_CHAT.bindTo(this._contextKeyService); this.cellResource = NOTEBOOK_CELL_RESOURCE.bindTo(this._contextKeyService); + this.cellHasErrorDiagnostics = NOTEBOOK_CELL_HAS_ERROR_DIAGNOSTICS.bindTo(this._contextKeyService); if (element) { this.updateForElement(element); @@ -200,6 +202,10 @@ export class CellContextKeyManager extends Disposable { this.cellRunState.set('idle'); this.cellExecuting.set(false); } + + if (this.element instanceof CodeCellViewModel) { + this.cellHasErrorDiagnostics.set(!!this.element.cellErrorDetails); + } } private updateForEditState() { diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts index f2ecbf4d4fc..517b47d7fe7 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts @@ -47,6 +47,9 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod private _outputCollection: number[] = []; private readonly _cellDiagnostics: CellDiagnostics; + get cellErrorDetails() { + return this._cellDiagnostics.ErrorDetails; + } private _outputsTop: PrefixSumComputer | null = null; @@ -171,7 +174,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod if (outputLayoutChange) { this.layoutChange({ outputHeight: true }, 'CodeCellViewModel#model.onDidChangeOutputs'); } - if (this._outputCollection.length === 0 && this._cellDiagnostics.ErrorDetails) { + if (this._outputCollection.length === 0) { this._cellDiagnostics.clear(); } dispose(removedOutputs); @@ -433,6 +436,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod protected onDidChangeTextModelContent(): void { if (this.getEditState() !== CellEditState.Editing) { + this._cellDiagnostics.clear(); this.updateEditState(CellEditState.Editing, 'onDidChangeTextModelContent'); this._onDidChangeState.fire({ contentChanged: true }); } diff --git a/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts b/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts index 8345a520e5c..a487c2c0fda 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts @@ -47,6 +47,7 @@ export const NOTEBOOK_CELL_INPUT_COLLAPSED = new RawContextKey('noteboo export const NOTEBOOK_CELL_OUTPUT_COLLAPSED = new RawContextKey('notebookCellOutputIsCollapsed', false); export const NOTEBOOK_CELL_RESOURCE = new RawContextKey('notebookCellResource', ''); export const NOTEBOOK_CELL_GENERATED_BY_CHAT = new RawContextKey('notebookCellGenerateByChat', false); +export const NOTEBOOK_CELL_HAS_ERROR_DIAGNOSTICS = new RawContextKey('notebookCellHasErrorDiagnostics', false); // Kernels export const NOTEBOOK_KERNEL = new RawContextKey('notebookKernel', undefined); diff --git a/src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts b/src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts index 33320b79444..3172a47ff34 100644 --- a/src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts @@ -254,7 +254,7 @@ export class TextSearchQuickAccess extends PickerQuickAccessProvider m === Message.ACCEPT_INPUT, this._store); readonly onDidCancelInput = Event.filter(this._messages.event, m => m === Message.CANCEL_INPUT || m === Message.CANCEL_SESSION, this._store); - private _terminalAgentId = 'terminal'; + private _terminalAgentName = 'terminal'; + private _terminalAgentId: string | undefined; private _model: MutableDisposable = this._register(new MutableDisposable()); @@ -97,7 +98,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr @IChatAccessibilityService private readonly _chatAccessibilityService: IChatAccessibilityService, @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, @IChatService private readonly _chatService: IChatService, - @IChatCodeBlockContextProviderService private readonly _chatCodeBlockContextProviderService: IChatCodeBlockContextProviderService + @IChatCodeBlockContextProviderService private readonly _chatCodeBlockContextProviderService: IChatCodeBlockContextProviderService, ) { super(); @@ -113,14 +114,8 @@ export class TerminalChatController extends Disposable implements ITerminalContr return; } - if (!this._chatAgentService.getAgent(this._terminalAgentId)) { - this._register(this._chatAgentService.onDidChangeAgents(() => { - if (this._chatAgentService.getAgent(this._terminalAgentId)) { - this._terminalAgentRegisteredContextKey.set(true); - } - })); - } else { - this._terminalAgentRegisteredContextKey.set(true); + if (!this.initTerminalAgent()) { + this._register(this._chatAgentService.onDidChangeAgents(() => this.initTerminalAgent())); } this._register(this._chatCodeBlockContextProviderService.registerProvider({ getCodeBlockContext: (editor) => { @@ -141,6 +136,17 @@ export class TerminalChatController extends Disposable implements ITerminalContr }, 'terminal')); } + private initTerminalAgent(): boolean { + const terminalAgent = this._chatAgentService.getAgentsByName(this._terminalAgentName)[0]; + if (terminalAgent) { + this._terminalAgentId = terminalAgent.id; + this._terminalAgentRegisteredContextKey.set(true); + return true; + } + + return false; + } + xtermReady(xterm: IXtermTerminal & { raw: RawXtermTerminal }): void { if (!this._configurationService.getValue(TerminalSettingId.ExperimentalInlineChat)) { return; @@ -286,13 +292,13 @@ export class TerminalChatController extends Disposable implements ITerminalContr const requestProps: IChatAgentRequest = { sessionId: model.sessionId, requestId: this._currentRequest!.id, - agentId: this._terminalAgentId, + agentId: this._terminalAgentId!, message: this._lastInput, variables: { variables: [] }, location: ChatAgentLocation.Terminal }; try { - const task = this._chatAgentService.invokeAgent(this._terminalAgentId, requestProps, progressCallback, getHistoryEntriesFromModel(model), cancellationToken); + const task = this._chatAgentService.invokeAgent(this._terminalAgentId!, requestProps, progressCallback, getHistoryEntriesFromModel(model), cancellationToken); this._chatWidget?.value.inlineChatWidget.updateChatMessage(undefined); this._chatWidget?.value.inlineChatWidget.updateFollowUps(undefined); this._chatWidget?.value.inlineChatWidget.updateProgress(true); diff --git a/src/vscode-dts/vscode.proposed.chatParticipant.d.ts b/src/vscode-dts/vscode.proposed.chatParticipant.d.ts index b2602cbf815..c4e92705f38 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipant.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipant.d.ts @@ -21,9 +21,9 @@ declare module 'vscode' { readonly prompt: string; /** - * The name of the chat participant and contributing extension to which this request was directed. + * The id of the chat participant and contributing extension to which this request was directed. */ - readonly participant: { readonly extensionId: string; readonly name: string }; + readonly participant: string; /** * The name of the {@link ChatCommand command} that was selected for this request. @@ -35,7 +35,7 @@ declare module 'vscode' { */ readonly variables: ChatResolvedVariable[]; - private constructor(prompt: string, command: string | undefined, variables: ChatResolvedVariable[], participant: { extensionId: string; name: string }); + private constructor(prompt: string, command: string | undefined, variables: ChatResolvedVariable[], participant: string); } /** @@ -54,16 +54,16 @@ declare module 'vscode' { readonly result: ChatResult; /** - * The name of the chat participant and contributing extension that this response came from. + * The id of the chat participant and contributing extension that this response came from. */ - readonly participant: { readonly extensionId: string; readonly name: string }; + readonly participant: string; /** * The name of the command that this response came from. */ readonly command?: string; - private constructor(response: ReadonlyArray, result: ChatResult, participant: { extensionId: string; name: string }); + private constructor(response: ReadonlyArray, result: ChatResult, participant: string); } export interface ChatContext { @@ -158,7 +158,7 @@ declare module 'vscode' { label?: string; /** - * By default, the followup goes to the same participant/command. But this property can be set to invoke a different participant. + * By default, the followup goes to the same participant/command. But this property can be set to invoke a different participant by ID. * Followups can only invoke a participant that was contributed by the same extension. */ participant?: string; @@ -192,9 +192,9 @@ declare module 'vscode' { */ export interface ChatParticipant { /** - * The short name by which this participant is referred to in the UI, e.g `workspace`. + * A unique ID for this participant. */ - readonly name: string; + readonly id: string; /** * Icon for the participant shown in UI. @@ -450,12 +450,11 @@ declare module 'vscode' { /** * Create a new {@link ChatParticipant chat participant} instance. * - * @param name Short name by which the participant is referred to in the UI. The name must be unique for the extension - * contributing the participant but can collide with names from other extensions. + * @param id A unique identifier for the participant. * @param handler A request handler for the participant. * @returns A new chat participant */ - export function createChatParticipant(name: string, handler: ChatRequestHandler): ChatParticipant; + export function createChatParticipant(id: string, handler: ChatRequestHandler): ChatParticipant; } /** diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index 3319ce9ca8a..2198ae4d90e 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -173,7 +173,9 @@ declare module 'vscode' { /** * Create a chat participant with the extended progress type */ - export function createChatParticipant(name: string, handler: ChatExtendedRequestHandler): ChatParticipant; + export function createChatParticipant(id: string, handler: ChatExtendedRequestHandler): ChatParticipant; + + export function createDynamicChatParticipant(id: string, name: string, description: string, handler: ChatExtendedRequestHandler): ChatParticipant; } /* @@ -280,12 +282,4 @@ declare module 'vscode' { */ resolve2?(name: string, context: ChatVariableContext, stream: ChatVariableResolverResponseStream, token: CancellationToken): ProviderResult; } - - export interface ChatParticipant { - /** - * A human-readable description explaining what this participant does. - * Only allow a static description for normal participants. Here where dynamic participants are allowed, the description must be able to be set as well. - */ - description?: string; - } }