From 966f94d38d017023568151892da7b5b350b6c62b Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 6 May 2024 18:09:24 +0200 Subject: [PATCH 001/357] update distro (#212102) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5f02a0964ad..aed4553b6dc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.90.0", - "distro": "5d328d131fa6f8c6923e6bdec0b6f538318c18e8", + "distro": "739fa8168a41653af56b597dae430dc56601dcaf", "author": { "name": "Microsoft Corporation" }, From 80e0aa45e098688a602c5070054ed7c38d5c778c Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Mon, 6 May 2024 09:10:38 -0700 Subject: [PATCH 002/357] Update notebook milestones (#212104) --- .vscode/notebooks/endgame.github-issues | 2 +- .vscode/notebooks/my-endgame.github-issues | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.vscode/notebooks/endgame.github-issues b/.vscode/notebooks/endgame.github-issues index 4fb94c29edd..d7836922bad 100644 --- a/.vscode/notebooks/endgame.github-issues +++ b/.vscode/notebooks/endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n$MILESTONE=milestone:\"April 2024\"" + "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n$MILESTONE=milestone:\"May 2024\"" }, { "kind": 1, diff --git a/.vscode/notebooks/my-endgame.github-issues b/.vscode/notebooks/my-endgame.github-issues index 3e48f05b3de..3bf56fffce4 100644 --- a/.vscode/notebooks/my-endgame.github-issues +++ b/.vscode/notebooks/my-endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n$MILESTONE=milestone:\"April 2024\"\n\n$MINE=assignee:@me" + "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n$MILESTONE=milestone:\"May 2024\"\n\n$MINE=assignee:@me" }, { "kind": 1, From e3d04f279f689a702b9e0939cb0d8dabbe309593 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Mon, 6 May 2024 09:47:37 -0700 Subject: [PATCH 003/357] cli: support refresh token in tunnel user login (#212106) --- cli/src/auth.rs | 9 +++++++-- cli/src/commands/args.rs | 7 +++++-- cli/src/commands/tunnels.rs | 12 +++++++++--- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/cli/src/auth.rs b/cli/src/auth.rs index 9d5c9b73fdb..67f1bfa6bc7 100644 --- a/cli/src/auth.rs +++ b/cli/src/auth.rs @@ -480,6 +480,7 @@ impl Auth { &self, provider: Option, access_token: Option, + refresh_token: Option, ) -> Result { let provider = match provider { Some(p) => p, @@ -490,8 +491,12 @@ impl Auth { Some(t) => StoredCredential { provider, access_token: t, - refresh_token: None, - expires_at: None, + // if a refresh token is given, assume it's valid now but refresh it + // soon in order to get the real expiry time. + expires_at: refresh_token + .as_ref() + .map(|_| Utc::now() + chrono::Duration::minutes(5)), + refresh_token, }, None => self.do_device_code_flow_with_provider(provider).await?, }; diff --git a/cli/src/commands/args.rs b/cli/src/commands/args.rs index 1eaaa57353e..8a943826615 100644 --- a/cli/src/commands/args.rs +++ b/cli/src/commands/args.rs @@ -788,11 +788,14 @@ pub enum TunnelUserSubCommands { #[derive(Args, Debug, Clone)] pub struct LoginArgs { - /// An access token to store for authentication. Note: this will not be - /// refreshed if it expires! + /// An access token to store for authentication. #[clap(long, requires = "provider")] pub access_token: Option, + /// An access token to store for authentication. + #[clap(long, requires = "access_token")] + pub refresh_token: Option, + /// The auth provider to use. If not provided, a prompt will be shown. #[clap(value_enum, long)] pub provider: Option, diff --git a/cli/src/commands/tunnels.rs b/cli/src/commands/tunnels.rs index f06cd9a1e2a..1755dbbfaef 100644 --- a/cli/src/commands/tunnels.rs +++ b/cli/src/commands/tunnels.rs @@ -274,10 +274,11 @@ pub async fn service( pub async fn user(ctx: CommandContext, user_args: TunnelUserSubCommands) -> Result { let auth = Auth::new(&ctx.paths, ctx.log.clone()); match user_args { - TunnelUserSubCommands::Login(login_args) => { + TunnelUserSubCommands::Login(mut login_args) => { auth.login( login_args.provider.map(|p| p.into()), - login_args.access_token.to_owned(), + login_args.access_token.take(), + login_args.refresh_token.take(), ) .await?; } @@ -488,7 +489,12 @@ pub async fn forward( forward_args.login.provider.take(), forward_args.login.access_token.take(), ) { - auth.login(Some(p.into()), Some(at)).await?; + auth.login( + Some(p.into()), + Some(at), + forward_args.login.refresh_token.take(), + ) + .await?; } let mut tunnels = DevTunnels::new_port_forwarding(&ctx.log, auth, &ctx.paths); From 8a3f7e95d207c49a6576b387d50a43d7ece7c585 Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Mon, 6 May 2024 10:00:49 -0700 Subject: [PATCH 004/357] feat: add disable-lcd-text flag (#211716) --- src/main.js | 5 ++++- src/vs/platform/environment/common/argv.ts | 1 + src/vs/platform/environment/node/argv.ts | 1 + src/vs/workbench/electron-sandbox/desktop.contribution.ts | 4 ++++ 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main.js b/src/main.js index 90de17b278b..9fe5654081d 100644 --- a/src/main.js +++ b/src/main.js @@ -202,7 +202,10 @@ function configureCommandlineSwitchesSync(cliArgs) { 'disable-hardware-acceleration', // override for the color profile to use - 'force-color-profile' + 'force-color-profile', + + // disable LCD font rendering, a Chromium flag + 'disable-lcd-text' ]; if (process.platform === 'linux') { diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts index f8d2ed05f8c..ba2a22ea621 100644 --- a/src/vs/platform/environment/common/argv.ts +++ b/src/vs/platform/environment/common/argv.ts @@ -129,6 +129,7 @@ export interface NativeParsedArgs { 'inspect'?: string; 'inspect-brk'?: string; 'js-flags'?: string; + 'disable-lcd-text'?: boolean; 'disable-gpu'?: boolean; 'disable-gpu-sandbox'?: boolean; 'nolazy'?: boolean; diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 91b5d5aeaba..3b6f55bd851 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -120,6 +120,7 @@ export const OPTIONS: OptionDescriptions> = { 'inspect-extensions': { type: 'string', allowEmptyValue: true, deprecates: ['debugPluginHost'], args: 'port', cat: 't', description: localize('inspect-extensions', "Allow debugging and profiling of extensions. Check the developer tools for the connection URI.") }, 'inspect-brk-extensions': { type: 'string', allowEmptyValue: true, deprecates: ['debugBrkPluginHost'], args: 'port', cat: 't', description: localize('inspect-brk-extensions', "Allow debugging and profiling of extensions with the extension host being paused after start. Check the developer tools for the connection URI.") }, + 'disable-lcd-text': { type: 'boolean', cat: 't', description: localize('disableLCDText', "Disable LCD font rendering.") }, 'disable-gpu': { type: 'boolean', cat: 't', description: localize('disableGPU', "Disable GPU hardware acceleration.") }, 'disable-chromium-sandbox': { type: 'boolean', cat: 't', description: localize('disableChromiumSandbox', "Use this option only when there is requirement to launch the application as sudo user on Linux or when running as an elevated user in an applocker environment on Windows.") }, 'sandbox': { type: 'boolean' }, diff --git a/src/vs/workbench/electron-sandbox/desktop.contribution.ts b/src/vs/workbench/electron-sandbox/desktop.contribution.ts index ff0aeec2494..2115ddfecc0 100644 --- a/src/vs/workbench/electron-sandbox/desktop.contribution.ts +++ b/src/vs/workbench/electron-sandbox/desktop.contribution.ts @@ -356,6 +356,10 @@ import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL } from 'vs/platform/window/electron-sand type: 'string', description: localize('argv.locale', 'The display Language to use. Picking a different language requires the associated language pack to be installed.') }, + 'disable-lcd-text': { + type: 'boolean', + description: localize('argv.disableLcdText', 'Disables LCD font antialiasing.') + }, 'disable-hardware-acceleration': { type: 'boolean', description: localize('argv.disableHardwareAcceleration', 'Disables hardware acceleration. ONLY change this option if you encounter graphic issues.') From 2826667d06cd45a56f2ab7be67ec8c194b4df7aa Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Mon, 6 May 2024 10:09:06 -0700 Subject: [PATCH 005/357] bump dev version (#212110) --- src/vs/platform/product/common/product.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/platform/product/common/product.ts b/src/vs/platform/product/common/product.ts index 467e11cc56a..eb383dadcb5 100644 --- a/src/vs/platform/product/common/product.ts +++ b/src/vs/platform/product/common/product.ts @@ -58,7 +58,7 @@ else { // Running out of sources if (Object.keys(product).length === 0) { Object.assign(product, { - version: '1.87.0-dev', + version: '1.90.0-dev', nameShort: 'Code - OSS Dev', nameLong: 'Code - OSS Dev', applicationName: 'code-oss', From 8444fa68ef44d5bb310b11afb9a8c32cbf2700e5 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 6 May 2024 20:27:25 +0200 Subject: [PATCH 006/357] notifications - improve wording for filter (#212117) --- .../browser/parts/notifications/notificationsCommands.ts | 2 +- .../browser/parts/notifications/notificationsViewer.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts b/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts index 6f205b9d81f..92b1b45d184 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts @@ -311,7 +311,7 @@ export function registerNotificationCommands(center: INotificationsCenterControl })); picker.canSelectMany = true; - picker.placeholder = localize('selectSources', "Select sources to enable notifications for"); + picker.placeholder = localize('selectSources', "Select sources to enable all notifications from"); picker.selectedItems = picker.items.filter(item => (item as INotificationSourceFilter).filter === NotificationsFilter.OFF) as (IQuickPickItem & INotificationSourceFilter)[]; picker.show(); diff --git a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts index a073b11dc83..65ad1afe6cc 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts @@ -249,7 +249,7 @@ export class NotificationRenderer implements IListRenderer that.notificationService.setFilter({ ...source, filter: isSourceFiltered ? NotificationsFilter.OFF : NotificationsFilter.ERROR }) })); From e50231341b17e94556e95a4f6faf800a20d5f258 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Mon, 6 May 2024 12:38:44 -0700 Subject: [PATCH 007/357] debug/testing: clean up most explicit type casts (#212119) For #211878 --- src/vs/base/common/objects.ts | 8 ++++ src/vs/base/test/common/objects.test.ts | 16 +++++++ .../api/browser/mainThreadDebugService.ts | 47 ++++++++++--------- .../api/common/extHostDebugService.ts | 20 ++++---- .../debug/browser/debugActionViewItems.ts | 4 +- .../debug/browser/watchExpressionsView.ts | 2 +- .../testing/browser/testingExplorerFilter.ts | 2 +- .../contrib/testing/common/testingStates.ts | 12 ++--- .../testing/test/browser/testObjectTree.ts | 2 +- 9 files changed, 68 insertions(+), 45 deletions(-) diff --git a/src/vs/base/common/objects.ts b/src/vs/base/common/objects.ts index a128a42b722..14ec0e71974 100644 --- a/src/vs/base/common/objects.ts +++ b/src/vs/base/common/objects.ts @@ -264,3 +264,11 @@ export function createProxyObject(methodNames: string[], invok } return result; } + +export function mapValues(obj: T, fn: (value: T[keyof T], key: string) => R): { [K in keyof T]: R } { + const result: { [key: string]: R } = {}; + for (const [key, value] of Object.entries(obj)) { + result[key] = fn(value, key); + } + return result as { [K in keyof T]: R }; +} diff --git a/src/vs/base/test/common/objects.test.ts b/src/vs/base/test/common/objects.test.ts index b3d0940f8fa..2465585544f 100644 --- a/src/vs/base/test/common/objects.test.ts +++ b/src/vs/base/test/common/objects.test.ts @@ -233,3 +233,19 @@ suite('Objects', () => { assert.strictEqual(obj1.mIxEdCaSe, objects.getCaseInsensitive(obj1, 'mixedcase')); }); }); + +test('mapValues', () => { + const obj = { + a: 1, + b: 2, + c: 3 + }; + + const result = objects.mapValues(obj, (value, key) => `${key}: ${value * 2}`); + + assert.deepStrictEqual(result, { + a: 'a: 2', + b: 'b: 4', + c: 'c: 6', + }); +}); diff --git a/src/vs/workbench/api/browser/mainThreadDebugService.ts b/src/vs/workbench/api/browser/mainThreadDebugService.ts index aea84bf4413..c5fe0f3c943 100644 --- a/src/vs/workbench/api/browser/mainThreadDebugService.ts +++ b/src/vs/workbench/api/browser/mainThreadDebugService.ts @@ -19,6 +19,7 @@ import { ErrorNoTelemetry } from 'vs/base/common/errors'; import { IDebugVisualizerService } from 'vs/workbench/contrib/debug/common/debugVisualizers'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { Event } from 'vs/base/common/event'; +import { isDefined } from 'vs/base/common/types'; @extHostNamedCustomer(MainContext.MainThreadDebugService) export class MainThreadDebugService implements MainThreadDebugServiceShape, IDebugAdapterFactory { @@ -210,18 +211,16 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb for (const dto of DTOs) { if (dto.type === 'sourceMulti') { - const rawbps = dto.lines.map(l => - { - id: l.id, - enabled: l.enabled, - lineNumber: l.line + 1, - column: l.character > 0 ? l.character + 1 : undefined, // a column value of 0 results in an omitted column attribute; see #46784 - condition: l.condition, - hitCondition: l.hitCondition, - logMessage: l.logMessage, - mode: l.mode, - } - ); + const rawbps = dto.lines.map((l): IBreakpointData => ({ + id: l.id, + enabled: l.enabled, + lineNumber: l.line + 1, + column: l.character > 0 ? l.character + 1 : undefined, // a column value of 0 results in an omitted column attribute; see #46784 + condition: l.condition, + hitCondition: l.hitCondition, + logMessage: l.logMessage, + mode: l.mode, + })); this.debugService.addBreakpoints(uri.revive(dto.uri), rawbps); } else if (dto.type === 'function') { this.debugService.addFunctionBreakpoint(dto.functionName, dto.id, dto.mode); @@ -248,7 +247,7 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb public $registerDebugConfigurationProvider(debugType: string, providerTriggerKind: DebugConfigurationProviderTriggerKind, hasProvide: boolean, hasResolve: boolean, hasResolve2: boolean, handle: number): Promise { - const provider = { + const provider: IDebugConfigurationProvider = { type: debugType, triggerKind: providerTriggerKind }; @@ -283,7 +282,7 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb public $registerDebugAdapterDescriptorFactory(debugType: string, handle: number): Promise { - const provider = { + const provider: IDebugAdapterDescriptorFactory = { type: debugType, createDebugAdapterDescriptor: session => { return Promise.resolve(this._proxy.$provideDebugAdapter(handle, this.getSessionDto(session))); @@ -435,8 +434,8 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb private convertToDto(bps: (ReadonlyArray)): Array { return bps.map(bp => { if ('name' in bp) { - const fbp = bp; - return { + const fbp: IFunctionBreakpoint = bp; + return { type: 'function', id: fbp.getId(), enabled: fbp.enabled, @@ -444,9 +443,9 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb hitCondition: fbp.hitCondition, logMessage: fbp.logMessage, functionName: fbp.name - }; + } satisfies IFunctionBreakpointDto; } else if ('src' in bp) { - const dbp = bp; + const dbp: IDataBreakpoint = bp; return { type: 'data', id: dbp.getId(), @@ -459,9 +458,9 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb label: dbp.description, canPersist: dbp.canPersist } satisfies IDataBreakpointDto; - } else { - const sbp = bp; - return { + } else if ('uri' in bp) { + const sbp: IBreakpoint = bp; + return { type: 'source', id: sbp.getId(), enabled: sbp.enabled, @@ -471,9 +470,11 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb uri: sbp.uri, line: sbp.lineNumber > 0 ? sbp.lineNumber - 1 : 0, character: (typeof sbp.column === 'number' && sbp.column > 0) ? sbp.column - 1 : 0, - }; + } satisfies ISourceBreakpointDto; + } else { + return undefined; } - }); + }).filter(isDefined); } } diff --git a/src/vs/workbench/api/common/extHostDebugService.ts b/src/vs/workbench/api/common/extHostDebugService.ts index d39a2dc0d8e..d5ea2bdf817 100644 --- a/src/vs/workbench/api/common/extHostDebugService.ts +++ b/src/vs/workbench/api/common/extHostDebugService.ts @@ -413,11 +413,11 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E if (bp instanceof SourceBreakpoint) { let dto = map.get(bp.location.uri.toString()); if (!dto) { - dto = { + dto = { type: 'sourceMulti', uri: bp.location.uri, lines: [] - }; + } satisfies ISourceMultiBreakpointDto; map.set(bp.location.uri.toString(), dto); dtos.push(dto); } @@ -883,28 +883,28 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E private convertToDto(x: vscode.DebugAdapterDescriptor): Dto { if (x instanceof DebugAdapterExecutable) { - return { + return { type: 'executable', command: x.command, args: x.args, options: x.options - }; + } satisfies IDebugAdapterExecutable; } else if (x instanceof DebugAdapterServer) { - return { + return { type: 'server', port: x.port, host: x.host - }; + } satisfies IDebugAdapterServer; } else if (x instanceof DebugAdapterNamedPipeServer) { - return { + return { type: 'pipeServer', path: x.path - }; + } satisfies IDebugAdapterNamedPipeServer; } else if (x instanceof DebugAdapterInlineImplementation) { - return >{ + return { type: 'implementation', implementation: x.implementation - }; + } as Dto; } else { throw new Error('convertToDto unexpected type'); } diff --git a/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts b/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts index 685db8b3b31..a4d5c4e74e2 100644 --- a/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts +++ b/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts @@ -259,7 +259,7 @@ export class StartDebugActionViewItem extends BaseActionViewItem { }); }); - this.selectBox.setOptions(this.debugOptions.map((data, index) => { text: data.label, isDisabled: disabledIdxs.indexOf(index) !== -1 }), this.selected); + this.selectBox.setOptions(this.debugOptions.map((data, index): ISelectOptionItem => ({ text: data.label, isDisabled: disabledIdxs.indexOf(index) !== -1 })), this.selected); } } @@ -314,7 +314,7 @@ export class FocusSessionActionViewItem extends SelectActionViewItem { text: data }), session ? sessions.indexOf(session) : undefined); + this.setOptions(names.map((data): ISelectOptionItem => ({ text: data })), session ? sessions.indexOf(session) : undefined); } private getSelectedSession(): IDebugSession | undefined { diff --git a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts index 8c6ec806b3a..7369a367607 100644 --- a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts +++ b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts @@ -421,7 +421,7 @@ class WatchExpressionsDragAndDrop implements ITreeDragAndDrop { } } - return { accept: true, effect: { type: ListDragOverEffectType.Move, position: dropEffectPosition }, feedback: [targetIndex] } as ITreeDragOverReaction; + return { accept: true, effect: { type: ListDragOverEffectType.Move, position: dropEffectPosition }, feedback: [targetIndex] } satisfies ITreeDragOverReaction; } getDragURI(element: IExpression): string | null { diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts index 16b2f5f1e92..6f90138b102 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts @@ -92,7 +92,7 @@ export class TestingExplorerFilter extends BaseActionViewItem { }); }), ].filter(r => !this.state.text.value.includes(r.label)), - } as SuggestResultsProvider, + } satisfies SuggestResultsProvider, resourceHandle: 'testing:filter', suggestOptions: { value: this.state.text.value, diff --git a/src/vs/workbench/contrib/testing/common/testingStates.ts b/src/vs/workbench/contrib/testing/common/testingStates.ts index 98b246bda2f..bcb75ddd6d7 100644 --- a/src/vs/workbench/contrib/testing/common/testingStates.ts +++ b/src/vs/workbench/contrib/testing/common/testingStates.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { mapValues } from 'vs/base/common/objects'; import { TestResultState } from 'vs/workbench/contrib/testing/common/testTypes'; export type TreeStateNode = { statusNode: true; state: TestResultState; priority: number }; @@ -25,13 +26,10 @@ export const statePriority: { [K in TestResultState]: number } = { export const isFailedState = (s: TestResultState) => s === TestResultState.Errored || s === TestResultState.Failed; export const isStateWithResult = (s: TestResultState) => s === TestResultState.Errored || s === TestResultState.Failed || s === TestResultState.Passed; -export const stateNodes = Object.entries(statePriority).reduce( - (acc, [stateStr, priority]) => { - const state = Number(stateStr) as TestResultState; - acc[state] = { statusNode: true, state, priority }; - return acc; - }, {} as { [K in TestResultState]: TreeStateNode } -); +export const stateNodes: { [K in TestResultState]: TreeStateNode } = mapValues(statePriority, (priority, stateStr): TreeStateNode => { + const state = Number(stateStr) as TestResultState; + return { statusNode: true, state, priority }; +}); export const cmpPriority = (a: TestResultState, b: TestResultState) => statePriority[b] - statePriority[a]; diff --git a/src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts b/src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts index e0923164c97..94b282e74b8 100644 --- a/src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts +++ b/src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts @@ -46,7 +46,7 @@ class TestObjectTree extends ObjectTree { disposeElement: (_el, _index, { store }) => store.clear(), renderTemplate: container => ({ container, store: new DisposableStore() }), templateId: 'default' - } as ITreeRenderer + } satisfies ITreeRenderer ], { sorter: sorter ?? { From cb823e57ab9a0a99f023c44f7f1116e9ff935365 Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Mon, 6 May 2024 12:39:14 -0700 Subject: [PATCH 008/357] Fix webview loading perf mark. (#212120) --- .../workbench/contrib/notebook/browser/notebookEditor.ts | 7 ++++++- .../contrib/notebook/browser/notebookEditorWidget.ts | 6 ++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index 3d306304664..473e8a1168d 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -430,6 +430,7 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane, I const startTime = perfMarks['startTime']; const extensionActivated = perfMarks['extensionActivated']; const inputLoaded = perfMarks['inputLoaded']; + const webviewCommLoaded = perfMarks['webviewCommLoaded']; const customMarkdownLoaded = perfMarks['customMarkdownLoaded']; const editorLoaded = perfMarks['editorLoaded']; @@ -444,7 +445,11 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane, I if (inputLoaded !== undefined) { inputLoadingTimespan = inputLoaded - extensionActivated; - webviewCommLoadingTimespan = inputLoaded - extensionActivated; // TODO@rebornix, we don't track webview comm anymore + } + + if (webviewCommLoaded !== undefined) { + webviewCommLoadingTimespan = webviewCommLoaded - extensionActivated; + } if (customMarkdownLoaded !== undefined) { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 3a65e207a93..23a4daf925a 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -1501,7 +1501,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD })); // init rendering - await this._warmupWithMarkdownRenderer(this.viewModel, viewState); + await this._warmupWithMarkdownRenderer(this.viewModel, viewState, perf); perf?.mark('customMarkdownLoaded'); @@ -1586,10 +1586,12 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._lastCellWithEditorFocus = cell; } - private async _warmupWithMarkdownRenderer(viewModel: NotebookViewModel, viewState: INotebookEditorViewState | undefined) { + private async _warmupWithMarkdownRenderer(viewModel: NotebookViewModel, viewState: INotebookEditorViewState | undefined, perf?: NotebookPerfMarks) { this.logService.debug('NotebookEditorWidget', 'warmup ' + this.viewModel?.uri.toString()); await this._resolveWebview(); + perf?.mark('webviewCommLoaded'); + this.logService.debug('NotebookEditorWidget', 'warmup - webview resolved'); // make sure that the webview is not visible otherwise users will see pre-rendered markdown cells in wrong position as the list view doesn't have a correct `top` offset yet From 93c5b127f02da307224698604104f8725002886e Mon Sep 17 00:00:00 2001 From: John Murray Date: Mon, 6 May 2024 21:59:36 +0100 Subject: [PATCH 009/357] Make codelenses work after switching from webview editor (fix #198309) (#211999) --- src/vs/editor/contrib/codelens/browser/codelensController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/contrib/codelens/browser/codelensController.ts b/src/vs/editor/contrib/codelens/browser/codelensController.ts index 2cfcb40b456..494336475f3 100644 --- a/src/vs/editor/contrib/codelens/browser/codelensController.ts +++ b/src/vs/editor/contrib/codelens/browser/codelensController.ts @@ -229,7 +229,7 @@ export class CodeLensContribution implements IEditorContribution { this._resolveCodeLensesPromise?.cancel(); this._resolveCodeLensesPromise = undefined; })); - this._localToDispose.add(this._editor.onDidFocusEditorWidget(() => { + this._localToDispose.add(this._editor.onDidFocusEditorText(() => { scheduler.schedule(); })); this._localToDispose.add(this._editor.onDidBlurEditorText(() => { From 3f91c9bcd730c75212c76ece0622ff95045dc67b Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Mon, 6 May 2024 13:59:56 -0700 Subject: [PATCH 010/357] Pick up latest TS for building VS Code (#210956) * Pick up latest TS for building VS Code * Update * Update yarn lock --- build/yarn.lock | 13 +++++++++++++ package.json | 2 +- src/vs/base/common/observableInternal/autorun.ts | 2 +- src/vs/base/common/observableInternal/derived.ts | 2 +- .../editor/common/services/findSectionHeaders.ts | 2 +- .../test/common/modes/supports/onEnterRules.ts | 6 +++--- .../contrib/notebook/common/notebookCommon.ts | 2 +- .../snippets/test/browser/snippetsService.test.ts | 4 ++-- yarn.lock | 14 +++++++------- 9 files changed, 30 insertions(+), 17 deletions(-) diff --git a/build/yarn.lock b/build/yarn.lock index 03248b45c22..cb4f5cfcae4 100644 --- a/build/yarn.lock +++ b/build/yarn.lock @@ -429,6 +429,11 @@ resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86" integrity sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA== +"@types/expect@^1.20.4": + version "1.20.4" + resolved "https://registry.yarnpkg.com/@types/expect/-/expect-1.20.4.tgz#8288e51737bf7e3ab5d7c77bfa695883745264e5" + integrity sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg== + "@types/fancy-log@^1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@types/fancy-log/-/fancy-log-1.3.0.tgz#a61ab476e5e628cd07a846330df53b85e05c8ce0" @@ -659,6 +664,14 @@ dependencies: "@types/node" "*" +"@types/vinyl@*": + version "2.0.12" + resolved "https://registry.yarnpkg.com/@types/vinyl/-/vinyl-2.0.12.tgz#17642ca9a8ae10f3db018e9f885da4188db4c6e6" + integrity sha512-Sr2fYMBUVGYq8kj3UthXFAu5UN6ZW+rYr4NACjZQJvHvj+c8lYv0CahmZ2P/r7iUkN44gGUBwqxZkrKXYPb7cw== + dependencies: + "@types/expect" "^1.20.4" + "@types/node" "*" + "@types/workerpool@^6.4.0": version "6.4.0" resolved "https://registry.yarnpkg.com/@types/workerpool/-/workerpool-6.4.0.tgz#c79292915dd08350d10e78e74687b6f401f270b8" diff --git a/package.json b/package.json index aed4553b6dc..e862cfcb31e 100644 --- a/package.json +++ b/package.json @@ -207,7 +207,7 @@ "ts-loader": "^9.4.2", "ts-node": "^10.9.1", "tsec": "0.2.7", - "typescript": "^5.5.0-dev.20240408", + "typescript": "^5.5.0-dev.20240506", "util": "^0.12.4", "vscode-nls-dev": "^3.3.1", "webpack": "^5.91.0", diff --git a/src/vs/base/common/observableInternal/autorun.ts b/src/vs/base/common/observableInternal/autorun.ts index 6c14cb20c5b..a2f169ee4d6 100644 --- a/src/vs/base/common/observableInternal/autorun.ts +++ b/src/vs/base/common/observableInternal/autorun.ts @@ -253,7 +253,7 @@ export class AutorunObserver implements IObserver, IReader const shouldReact = this._handleChange ? this._handleChange({ changedObservable: observable, change, - didChange: o => o === observable as any, + didChange: (o): this is any => o === observable as any, }, this.changeSummary!) : true; if (shouldReact) { this.state = AutorunState.stale; diff --git a/src/vs/base/common/observableInternal/derived.ts b/src/vs/base/common/observableInternal/derived.ts index 4111141589c..9e95bf9dccc 100644 --- a/src/vs/base/common/observableInternal/derived.ts +++ b/src/vs/base/common/observableInternal/derived.ts @@ -350,7 +350,7 @@ export class Derived extends BaseObservable im const shouldReact = this._handleChange ? this._handleChange({ changedObservable: observable, change, - didChange: o => o === observable as any, + didChange: (o): this is any => o === observable as any, }, this.changeSummary!) : true; const wasUpToDate = this.state === DerivedState.upToDate; if (shouldReact && (this.state === DerivedState.dependenciesMightHaveChanged || wasUpToDate)) { diff --git a/src/vs/editor/common/services/findSectionHeaders.ts b/src/vs/editor/common/services/findSectionHeaders.ts index 08bd3709741..8e03723d0dd 100644 --- a/src/vs/editor/common/services/findSectionHeaders.ts +++ b/src/vs/editor/common/services/findSectionHeaders.ts @@ -36,7 +36,7 @@ export interface SectionHeader { shouldBeInComments: boolean; } -const markRegex = /\bMARK:\s*(.*)$/d; +const markRegex = new RegExp('\\bMARK:\\s*(.*)$', 'd'); const trimDashesRegex = /^-+|-+$/g; /** diff --git a/src/vs/editor/test/common/modes/supports/onEnterRules.ts b/src/vs/editor/test/common/modes/supports/onEnterRules.ts index 94869ad640f..66e683fdcf8 100644 --- a/src/vs/editor/test/common/modes/supports/onEnterRules.ts +++ b/src/vs/editor/test/common/modes/supports/onEnterRules.ts @@ -118,14 +118,14 @@ export const cppOnEnterRules = [ export const htmlOnEnterRules = [ { - beforeText: /<(?!(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr))([_:\w][_:\w-.\d]*)(?:(?:[^'"/>]|"[^"]*"|'[^']*')*?(?!\/)>)[^<]*$/i, - afterText: /^<\/([_:\w][_:\w-.\d]*)\s*>/i, + beforeText: /<(?!(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr))([_:\w][_:\w\-.\d]*)(?:(?:[^'"/>]|"[^"]*"|'[^']*')*?(?!\/)>)[^<]*$/i, + afterText: /^<\/([_:\w][_:\w\-.\d]*)\s*>/i, action: { indentAction: IndentAction.IndentOutdent } }, { - beforeText: /<(?!(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr))([_:\w][_:\w-.\d]*)(?:(?:[^'"/>]|"[^"]*"|'[^']*')*?(?!\/)>)[^<]*$/i, + beforeText: /<(?!(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr))([_:\w][_:\w\-.\d]*)(?:(?:[^'"/>]|"[^"]*"|'[^']*')*?(?!\/)>)[^<]*$/i, action: { indentAction: IndentAction.Indent } diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 67315a1f315..c269db86ed4 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -790,7 +790,7 @@ export interface INotebookEditorModel extends IDisposable { readonly viewType: string; readonly notebook: INotebookTextModel | undefined; readonly hasErrorState: boolean; - isResolved(): this is IResolvedNotebookEditorModel; + isResolved(): boolean; isDirty(): boolean; isModified(): boolean; isReadonly(): boolean | IMarkdownString; diff --git a/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts b/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts index 82693d67bdd..ce61496ec51 100644 --- a/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts +++ b/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts @@ -563,10 +563,10 @@ suite('SnippetsService', function () { assert.strictEqual(completions.items.length, 1); }); - test('issue #61296: VS code freezes when editing CSS file with emoji', async function () { + test('issue #61296: VS code freezes when editing CSS fi`le with emoji', async function () { const languageConfigurationService = disposables.add(new TestLanguageConfigurationService()); disposables.add(languageConfigurationService.register('fooLang', { - wordPattern: /(#?-?\d*\.\d\w*%?)|(::?[\w-]*(?=[^,{;]*[,{]))|(([@#.!])?[\w-?]+%?|[@#!.])/g + wordPattern: /(#?-?\d*\.\d\w*%?)|(::?[\w-]*(?=[^,{;]*[,{]))|(([@#.!])?[\w\-?]+%?|[@#!.])/g })); snippetService = new SimpleSnippetService([new Snippet( diff --git a/yarn.lock b/yarn.lock index d9afb5f8499..eced3b8d5a3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1322,9 +1322,9 @@ "@types/node" "*" "@types/vinyl@*": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@types/vinyl/-/vinyl-2.0.4.tgz#9a7a8071c8d14d3a95d41ebe7135babe4ad5995a" - integrity sha512-2o6a2ixaVI2EbwBPg1QYLGQoHK56p/8X/sGfKbFC8N6sY9lfjsMf/GprtkQkSya0D4uRiutRZ2BWj7k3JvLsAQ== + version "2.0.12" + resolved "https://registry.yarnpkg.com/@types/vinyl/-/vinyl-2.0.12.tgz#17642ca9a8ae10f3db018e9f885da4188db4c6e6" + integrity sha512-Sr2fYMBUVGYq8kj3UthXFAu5UN6ZW+rYr4NACjZQJvHvj+c8lYv0CahmZ2P/r7iUkN44gGUBwqxZkrKXYPb7cw== dependencies: "@types/expect" "^1.20.4" "@types/node" "*" @@ -10043,10 +10043,10 @@ typescript@^4.7.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6" integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ== -typescript@^5.5.0-dev.20240408: - version "5.5.0-dev.20240408" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.0-dev.20240408.tgz#337832c87cf0db5a11f9efcff9c789a982ea77c4" - integrity sha512-WCqFA68PbE0+khOu6x2LPxePy0tKdWuNO2m2K4A/L+OPqua1Qmck9OXUQ/5nUd4B/8UlBuhkhuulQbr2LHO9vA== +typescript@^5.5.0-dev.20240506: + version "5.5.0-dev.20240506" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.0-dev.20240506.tgz#d46aac8be07432092e3bd7e7fb7aae93b21d738a" + integrity sha512-0lnovJfyTASSjJvryIfT3sYDXAv1n2R0vujhtdXQiAxA+PRpCOTk7UqslELD6wl7t3s9hH5UI/+p5aPeSpmbYw== typical@^4.0.0: version "4.0.0" From 26120e5bf439adda1592efd82beb5ae1a970182e Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Mon, 6 May 2024 15:54:33 -0700 Subject: [PATCH 011/357] testing: add temporary failure tracker to the selfhost test runner (#212134) For /fixTestFailures, I want to get more 'real world' tests and test fixes. This makes a change in the selfhost test provider such that when a test fails and is then fixed, we record the code changes into a JSON file in the `.build` directory. In a few days I'll follow up with team members to collect their test failures and use them as evaluation tests for copilot. The FailureTracker will be removed when I've gotten enough data. --- .../.vscode/launch.json | 11 ++ .../src/extension.ts | 10 +- .../src/failureTracker.ts | 126 ++++++++++++++++++ build/lib/watch/watch-win32.js | 4 +- build/lib/watch/watch-win32.ts | 6 +- 5 files changed, 151 insertions(+), 6 deletions(-) create mode 100644 .vscode/extensions/vscode-selfhost-test-provider/.vscode/launch.json create mode 100644 .vscode/extensions/vscode-selfhost-test-provider/src/failureTracker.ts diff --git a/.vscode/extensions/vscode-selfhost-test-provider/.vscode/launch.json b/.vscode/extensions/vscode-selfhost-test-provider/.vscode/launch.json new file mode 100644 index 00000000000..fab178dfaeb --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/.vscode/launch.json @@ -0,0 +1,11 @@ +{ + "configurations": [ + { + "args": ["--extensionDevelopmentPath=${workspaceFolder}"], + "name": "Launch Extension", + "outFiles": ["${workspaceFolder}/out/**/*.js"], + "request": "launch", + "type": "extensionHost" + } + ] +} diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts index b4fa3310545..153f5efb2d9 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts @@ -19,6 +19,7 @@ import { itemData, } from './testTree'; import { BrowserTestRunner, PlatformTestRunner, VSCodeTestRunner } from './vscodeTestRunner'; +import { FailureTracker } from './failureTracker'; const TEST_FILE_PATTERN = 'src/vs/**/*.{test,integrationTest}.ts'; @@ -54,6 +55,8 @@ export async function activate(context: vscode.ExtensionContext) { } }; + let startedTrackingFailures = false; + const createRunHandler = ( runnerCtor: { new(folder: vscode.WorkspaceFolder): VSCodeTestRunner }, kind: vscode.TestRunProfileKind, @@ -68,6 +71,11 @@ export async function activate(context: vscode.ExtensionContext) { return; } + if (!startedTrackingFailures) { + startedTrackingFailures = true; + context.subscriptions.push(new FailureTracker(folder.uri.fsPath)); + } + const runner = new runnerCtor(folder); const map = await getPendingTestMap(ctrl, req.include ?? gatherTestItems(ctrl.items)); const task = ctrl.createTestRun(req); @@ -225,7 +233,7 @@ export async function activate(context: vscode.ExtensionContext) { vscode.workspace.onDidOpenTextDocument(updateNodeForDocument), vscode.workspace.onDidChangeTextDocument(e => updateNodeForDocument(e.document)), registerSnapshotUpdate(ctrl), - new FailingDeepStrictEqualAssertFixer() + new FailingDeepStrictEqualAssertFixer(), ); } diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/failureTracker.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/failureTracker.ts new file mode 100644 index 00000000000..a3e4c08530d --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/failureTracker.ts @@ -0,0 +1,126 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { spawn } from 'child_process'; +import { readFile, writeFile } from 'fs/promises'; +import { join } from 'path'; +import * as vscode from 'vscode'; + +interface IGitState { + commitId: string; + tracked: string; + untracked: Record; +} + +interface ITrackedRemediation { + snapshot: vscode.TestResultSnapshot; + failing: IGitState; + passing: IGitState; +} + +const MAX_FAILURES = 10; + +export class FailureTracker { + private readonly disposables: vscode.Disposable[] = []; + private readonly lastFailed = new Map< + string, + { snapshot: vscode.TestResultSnapshot; failing: IGitState } + >(); + + private readonly logFile = join(this.rootDir, '.build/vscode-test-failures.json'); + private logs?: ITrackedRemediation[]; + + constructor(private readonly rootDir: string) { + this.disposables.push( + vscode.tests.onDidChangeTestResults(() => { + const last = vscode.tests.testResults[0]; + if (!last) { + return; + } + + let gitState: Promise | undefined; + const getGitState = () => gitState ?? (gitState = this.captureGitState()); + + const queue = [last.results]; + for (let i = 0; i < queue.length; i++) { + for (const snapshot of queue[i]) { + // only interested in states of leaf tests + if (snapshot.children.length) { + queue.push(snapshot.children); + continue; + } + + const key = `${snapshot.uri}/${snapshot.id}`; + const prev = this.lastFailed.get(key); + if (snapshot.taskStates.some(s => s.state === vscode.TestResultState.Failed)) { + // unset the parent to avoid a circular JSON structure: + getGitState().then(s => this.lastFailed.set(key, { snapshot: { ...snapshot, parent: undefined }, failing: s })); + } else if (prev) { + this.lastFailed.delete(key); + getGitState().then(s => this.append({ ...prev, passing: s })); + } + } + } + }) + ); + } + + private async append(log: ITrackedRemediation) { + if (!this.logs) { + try { + this.logs = JSON.parse(await readFile(this.logFile, 'utf-8')); + } catch { + this.logs = []; + } + } + + const logs = this.logs!; + logs.push(log); + if (logs.length > MAX_FAILURES) { + logs.splice(0, logs.length - MAX_FAILURES); + } + + await writeFile(this.logFile, JSON.stringify(logs, undefined, 2)); + } + + private async captureGitState() { + const [commitId, tracked, untracked] = await Promise.all([ + this.exec('git', ['rev-parse', 'HEAD']), + this.exec('git', ['diff', 'HEAD']), + this.exec('git', ['ls-files', '--others', '--exclude-standard']).then(async output => { + const mapping: Record = {}; + await Promise.all( + output + .trim() + .split('\n') + .map(async f => { + mapping[f] = await readFile(join(this.rootDir, f), 'utf-8'); + }) + ); + return mapping; + }), + ]); + return { commitId, tracked, untracked }; + } + + public dispose() { + this.disposables.forEach(d => d.dispose()); + } + + private exec(command: string, args: string[]): Promise { + return new Promise((resolve, reject) => { + const child = spawn(command, args, { stdio: 'pipe', cwd: this.rootDir }); + let output = ''; + child.stdout.setEncoding('utf-8').on('data', b => (output += b)); + child.stderr.setEncoding('utf-8').on('data', b => (output += b)); + child.on('error', reject); + child.on('exit', code => + code === 0 + ? resolve(output) + : reject(new Error(`Failed with error code ${code}\n${output}`)) + ); + }); + } +} diff --git a/build/lib/watch/watch-win32.js b/build/lib/watch/watch-win32.js index 49094e915e4..934d8e8110f 100644 --- a/build/lib/watch/watch-win32.js +++ b/build/lib/watch/watch-win32.js @@ -70,8 +70,8 @@ module.exports = function (pattern, options) { return f; }); return watcher - .pipe(filter(['**', '!.git{,/**}'])) // ignore all things git - .pipe(filter(pattern)) + .pipe(filter(['**', '!.git{,/**}'], { dot: options.dot })) // ignore all things git + .pipe(filter(pattern, { dot: options.dot })) .pipe(es.map(function (file, cb) { fs.stat(file.path, function (err, stat) { if (err && err.code === 'ENOENT') { diff --git a/build/lib/watch/watch-win32.ts b/build/lib/watch/watch-win32.ts index fa65a5bdeb2..afde6a79f22 100644 --- a/build/lib/watch/watch-win32.ts +++ b/build/lib/watch/watch-win32.ts @@ -70,7 +70,7 @@ function watch(root: string): Stream { const cache: { [cwd: string]: Stream } = Object.create(null); -module.exports = function (pattern: string | string[] | filter.FileFunction, options?: { cwd?: string; base?: string }) { +module.exports = function (pattern: string | string[] | filter.FileFunction, options?: { cwd?: string; base?: string; dot?: boolean }) { options = options || {}; const cwd = path.normalize(options.cwd || process.cwd()); @@ -86,8 +86,8 @@ module.exports = function (pattern: string | string[] | filter.FileFunction, opt }); return watcher - .pipe(filter(['**', '!.git{,/**}'])) // ignore all things git - .pipe(filter(pattern)) + .pipe(filter(['**', '!.git{,/**}'], { dot: options.dot })) // ignore all things git + .pipe(filter(pattern, { dot: options.dot })) .pipe(es.map(function (file: File, cb) { fs.stat(file.path, function (err, stat) { if (err && err.code === 'ENOENT') { return cb(undefined, file); } From 00bc1553bafe83896a96e1b2572024083d14515a Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Mon, 6 May 2024 20:48:04 -0700 Subject: [PATCH 012/357] create `AccessibleViewRegistry`, move all applicable views/accessibility help dialogs over to use it (#212138) --- .../hover/browser/hoverAccessibleViews.ts | 66 +++ .../hover/browser/hoverContribution.ts | 4 + .../browser/inlineCompletions.contribution.ts | 4 + .../inlineCompletionsAccessibleView.ts | 62 +++ .../accessibility/browser/accessibleView.ts | 166 +++++++ .../browser/accessibleViewRegistry.ts | 54 +++ .../notificationAccessibleView.ts | 133 ++++++ src/vs/workbench/browser/workbench.ts | 5 + .../browser/accessibility.contribution.ts | 14 +- .../browser/accessibilityConfiguration.ts | 17 - .../accessibility/browser/accessibleView.ts | 150 +------ .../browser/accessibleViewActions.ts | 4 +- .../browser/accessibleViewContributions.ts | 418 +----------------- .../browser/editorAccessibilityHelp.ts | 9 +- .../extensionAccesibilityHelp.contribution.ts | 94 ++++ .../browser/actions/chatAccessibilityHelp.ts | 26 +- .../chat/browser/actions/chatActions.ts | 23 +- .../contrib/chat/browser/chat.contribution.ts | 101 +---- .../chat/browser/chatAccessibilityProvider.ts | 2 +- .../browser/chatResponseAccessibleView.ts | 97 ++++ .../browser/diffEditorAccessibilityHelp.ts | 73 +++ .../codeEditor/browser/diffEditorHelper.ts | 70 +-- .../comments/browser/comments.contribution.ts | 9 +- .../comments/browser/commentsAccessibility.ts | 26 +- .../browser/commentsAccessibleView.ts | 87 ++++ .../browser/commentsEditorContribution.ts | 3 +- .../browser/inlineChat.contribution.ts | 11 +- .../browser/inlineChatAccessibilityHelp.ts | 26 ++ .../browser/inlineChatAccessibleView.ts | 67 ++- .../inlineChat/browser/inlineChatActions.ts | 17 +- .../inlineChat/browser/inlineChatWidget.ts | 2 +- .../test/browser/inlineChatController.test.ts | 2 +- .../test/browser/inlineChatSession.test.ts | 2 +- .../notebook/browser/notebook.contribution.ts | 44 +- .../notebook/browser/notebookAccessibility.ts | 124 ------ .../browser/notebookAccessibilityHelp.ts | 86 ++++ .../browser/notebookAccessibleView.ts | 86 ++++ .../terminal/browser/terminalActions.ts | 3 +- .../browser/terminalRunRecentQuickPick.ts | 3 +- .../terminal.accessibility.contribution.ts | 4 +- .../browser/terminalAccessibilityHelp.ts | 4 +- .../terminalAccessibleBufferProvider.ts | 4 +- .../browser/terminal.chat.contribution.ts | 10 +- .../browser/terminalChatAccessibilityHelp.ts | 52 +-- .../browser/terminalChatAccessibleView.ts | 54 ++- .../browser/terminal.links.contribution.ts | 3 +- .../links/browser/terminalLinkQuickpick.ts | 3 +- 47 files changed, 1262 insertions(+), 1062 deletions(-) create mode 100644 src/vs/editor/contrib/hover/browser/hoverAccessibleViews.ts create mode 100644 src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts create mode 100644 src/vs/platform/accessibility/browser/accessibleView.ts create mode 100644 src/vs/platform/accessibility/browser/accessibleViewRegistry.ts create mode 100644 src/vs/workbench/browser/parts/notifications/notificationAccessibleView.ts create mode 100644 src/vs/workbench/contrib/accessibility/browser/extensionAccesibilityHelp.contribution.ts create mode 100644 src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts create mode 100644 src/vs/workbench/contrib/codeEditor/browser/diffEditorAccessibilityHelp.ts create mode 100644 src/vs/workbench/contrib/comments/browser/commentsAccessibleView.ts create mode 100644 src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibilityHelp.ts create mode 100644 src/vs/workbench/contrib/notebook/browser/notebookAccessibilityHelp.ts create mode 100644 src/vs/workbench/contrib/notebook/browser/notebookAccessibleView.ts diff --git a/src/vs/editor/contrib/hover/browser/hoverAccessibleViews.ts b/src/vs/editor/contrib/hover/browser/hoverAccessibleViews.ts new file mode 100644 index 00000000000..557f1fffa85 --- /dev/null +++ b/src/vs/editor/contrib/hover/browser/hoverAccessibleViews.ts @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; +import { AccessibleViewType, AccessibleViewProviderId } from 'vs/platform/accessibility/browser/accessibleView'; +import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; + +export class HoverAccessibleView implements IAccessibleViewImplentation { + readonly type = AccessibleViewType.View; + readonly priority = 95; + readonly name = 'hover'; + readonly when = EditorContextKeys.hoverFocused; + getProvider(accessor: ServicesAccessor) { + const codeEditorService = accessor.get(ICodeEditorService); + const editor = codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor(); + const editorHoverContent = editor ? HoverController.get(editor)?.getWidgetContent() ?? undefined : undefined; + if (!editor || !editorHoverContent) { + return; + } + return { + id: AccessibleViewProviderId.Hover, + verbositySettingKey: 'accessibility.verbosity.hover', + provideContent() { return editorHoverContent; }, + onClose() { + HoverController.get(editor)?.focus(); + }, + options: { + language: editor?.getModel()?.getLanguageId() ?? 'typescript', + type: AccessibleViewType.View + } + }; + } +} + +export class ExtHoverAccessibleView implements IAccessibleViewImplentation { + readonly type = AccessibleViewType.View; + readonly priority = 90; + readonly name = 'extension-hover'; + getProvider(accessor: ServicesAccessor) { + const contextViewService = accessor.get(IContextViewService); + const contextViewElement = contextViewService.getContextViewElement(); + const extensionHoverContent = contextViewElement?.textContent ?? undefined; + const hoverService = accessor.get(IHoverService); + + if (contextViewElement.classList.contains('accessible-view-container') || !extensionHoverContent) { + // The accessible view, itself, uses the context view service to display the text. We don't want to read that. + return; + } + return { + id: AccessibleViewProviderId.Hover, + verbositySettingKey: 'accessibility.verbosity.hover', + provideContent() { return extensionHoverContent; }, + onClose() { + hoverService.showAndFocusLastHover(); + }, + options: { language: 'typescript', type: AccessibleViewType.View } + }; + } +} diff --git a/src/vs/editor/contrib/hover/browser/hoverContribution.ts b/src/vs/editor/contrib/hover/browser/hoverContribution.ts index 33f5cd8f316..629b189f2db 100644 --- a/src/vs/editor/contrib/hover/browser/hoverContribution.ts +++ b/src/vs/editor/contrib/hover/browser/hoverContribution.ts @@ -12,6 +12,8 @@ import { MarkdownHoverParticipant } from 'vs/editor/contrib/hover/browser/markdo import { MarkerHoverParticipant } from 'vs/editor/contrib/hover/browser/markerHoverParticipant'; import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; import 'vs/css!./hover'; +import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { ExtHoverAccessibleView, HoverAccessibleView } from 'vs/editor/contrib/hover/browser/hoverAccessibleViews'; registerEditorContribution(HoverController.ID, HoverController, EditorContributionInstantiation.BeforeFirstInteraction); registerEditorAction(ShowOrFocusHoverAction); @@ -38,3 +40,5 @@ registerThemingParticipant((theme, collector) => { collector.addRule(`.monaco-editor .monaco-hover hr { border-bottom: 0px solid ${hoverBorder.transparent(0.5)}; }`); } }); +AccessibleViewRegistry.register(new HoverAccessibleView()); +AccessibleViewRegistry.register(new ExtHoverAccessibleView()); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletions.contribution.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletions.contribution.ts index ff80048cc4d..2819af72061 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletions.contribution.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletions.contribution.ts @@ -7,7 +7,9 @@ import { EditorContributionInstantiation, registerEditorAction, registerEditorCo import { HoverParticipantRegistry } from 'vs/editor/contrib/hover/browser/hoverTypes'; import { TriggerInlineSuggestionAction, ShowNextInlineSuggestionAction, ShowPreviousInlineSuggestionAction, AcceptNextWordOfInlineCompletion, AcceptInlineCompletion, HideInlineCompletion, ToggleAlwaysShowInlineSuggestionToolbar, AcceptNextLineOfInlineCompletion } from 'vs/editor/contrib/inlineCompletions/browser/commands'; import { InlineCompletionsHoverParticipant } from 'vs/editor/contrib/inlineCompletions/browser/hoverParticipant'; +import { InlineCompletionsAccessibleView } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView'; import { InlineCompletionsController } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController'; +import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; import { registerAction2 } from 'vs/platform/actions/common/actions'; registerEditorContribution(InlineCompletionsController.ID, InlineCompletionsController, EditorContributionInstantiation.Eventually); @@ -22,3 +24,5 @@ registerEditorAction(HideInlineCompletion); registerAction2(ToggleAlwaysShowInlineSuggestionToolbar); HoverParticipantRegistry.register(InlineCompletionsHoverParticipant); + +AccessibleViewRegistry.register(new InlineCompletionsAccessibleView()); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts new file mode 100644 index 00000000000..6182681a3b5 --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { InlineCompletionContextKeys } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionContextKeys'; +import { InlineCompletionsController } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController'; +import { AccessibleViewType, AccessibleViewProviderId } from 'vs/platform/accessibility/browser/accessibleView'; +import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; + +export class InlineCompletionsAccessibleView extends Disposable implements IAccessibleViewImplentation { + readonly type = AccessibleViewType.View; + readonly priority = 95; + readonly name = 'inline-completions'; + readonly when = ContextKeyExpr.and(InlineCompletionContextKeys.inlineSuggestionVisible); + getProvider(accessor: ServicesAccessor) { + const codeEditorService = accessor.get(ICodeEditorService); + function resolveProvider() { + const editor = codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor(); + if (!editor) { + return; + } + const model = InlineCompletionsController.get(editor)?.model.get(); + const state = model?.state.get(); + if (!model || !state) { + return; + } + const lineText = model.textModel.getLineContent(state.primaryGhostText.lineNumber); + const ghostText = state.primaryGhostText.renderForScreenReader(lineText); + if (!ghostText) { + return; + } + const language = editor.getModel()?.getLanguageId() ?? undefined; + return { + id: AccessibleViewProviderId.InlineCompletions, + verbositySettingKey: 'accessibility.verbosity.inlineCompletions', + provideContent() { return lineText + ghostText; }, + onClose() { + model.stop(); + editor.focus(); + }, + next() { + model.next(); + setTimeout(() => resolveProvider(), 50); + }, + previous() { + model.previous(); + setTimeout(() => resolveProvider(), 50); + }, + options: { language, type: AccessibleViewType.View } + }; + } + return resolveProvider(); + } + constructor() { + super(); + } +} diff --git a/src/vs/platform/accessibility/browser/accessibleView.ts b/src/vs/platform/accessibility/browser/accessibleView.ts new file mode 100644 index 00000000000..34d2b431a08 --- /dev/null +++ b/src/vs/platform/accessibility/browser/accessibleView.ts @@ -0,0 +1,166 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding'; +import { IPickerQuickAccessItem } from 'vs/platform/quickinput/browser/pickerQuickAccess'; +import { Event } from 'vs/base/common/event'; +import { IAction } from 'vs/base/common/actions'; + +export const IAccessibleViewService = createDecorator('accessibleViewService'); + +export const enum AccessibleViewProviderId { + Terminal = 'terminal', + TerminalChat = 'terminal-chat', + TerminalHelp = 'terminal-help', + DiffEditor = 'diffEditor', + Chat = 'panelChat', + InlineChat = 'inlineChat', + InlineCompletions = 'inlineCompletions', + KeybindingsEditor = 'keybindingsEditor', + Notebook = 'notebook', + Editor = 'editor', + Hover = 'hover', + Notification = 'notification', + EmptyEditorHint = 'emptyEditorHint', + Comments = 'comments' +} + +export const enum AccessibleViewType { + Help = 'help', + View = 'view' +} + +export const enum NavigationType { + Previous = 'previous', + Next = 'next' +} + +export interface IAccessibleViewOptions { + readMoreUrl?: string; + /** + * Defaults to markdown + */ + language?: string; + type: AccessibleViewType; + /** + * By default, places the cursor on the top line of the accessible view. + * If set to 'initial-bottom', places the cursor on the bottom line of the accessible view and preserves it henceforth. + * If set to 'bottom', places the cursor on the bottom line of the accessible view. + */ + position?: 'bottom' | 'initial-bottom'; + /** + * @returns a string that will be used as the content of the help dialog + * instead of the one provided by default. + */ + customHelp?: () => string; + /** + * If this provider might want to request to be shown again, provide an ID. + */ + id?: AccessibleViewProviderId; +} + + +export interface IAccessibleViewContentProvider extends IBasicContentProvider { + id: AccessibleViewProviderId; + verbositySettingKey: string; + /** + * Note that a Codicon class should be provided for each action. + * If not, a default will be used. + */ + onKeyDown?(e: IKeyboardEvent): void; + /** + * When the language is markdown, this is provided by default. + */ + getSymbols?(): IAccessibleViewSymbol[]; + /** + * Note that this will only take effect if the provider has an ID. + */ + onDidRequestClearLastProvider?: Event; +} + + +export interface IAccessibleViewSymbol extends IPickerQuickAccessItem { + markdownToParse?: string; + firstListItem?: string; + lineNumber?: number; + endLineNumber?: number; +} + +export interface IPosition { + lineNumber: number; + column: number; +} + +export interface IAccessibleViewService { + readonly _serviceBrand: undefined; + show(provider: AccesibleViewContentProvider, position?: IPosition): void; + showLastProvider(id: AccessibleViewProviderId): void; + showAccessibleViewHelp(): void; + next(): void; + previous(): void; + navigateToCodeBlock(type: 'next' | 'previous'): void; + goToSymbol(): void; + disableHint(): void; + getPosition(id: AccessibleViewProviderId): IPosition | undefined; + setPosition(position: IPosition, reveal?: boolean): void; + getLastPosition(): IPosition | undefined; + /** + * If the setting is enabled, provides the open accessible view hint as a localized string. + * @param verbositySettingKey The setting key for the verbosity of the feature + */ + getOpenAriaHint(verbositySettingKey: string): string | null; + getCodeBlockContext(): ICodeBlockActionContext | undefined; +} + + +export interface ICodeBlockActionContext { + code: string; + languageId?: string; + codeBlockIndex: number; + element: unknown; +} + +export type AccesibleViewContentProvider = AdvancedContentProvider | ExtensionContentProvider; + +export class AdvancedContentProvider implements IAccessibleViewContentProvider { + + constructor( + public id: AccessibleViewProviderId, + public options: IAccessibleViewOptions, + public provideContent: () => string, + public onClose: () => void, + public verbositySettingKey: string, + public actions?: IAction[], + public next?: () => void, + public previous?: () => void, + public onKeyDown?: (e: IKeyboardEvent) => void, + public getSymbols?: () => IAccessibleViewSymbol[], + public onDidRequestClearLastProvider?: Event, + ) { } +} + +export class ExtensionContentProvider implements IBasicContentProvider { + + constructor( + public readonly id: string, + public options: IAccessibleViewOptions, + public provideContent: () => string, + public onClose: () => void, + public next?: () => void, + public previous?: () => void, + public actions?: IAction[], + ) { } +} + +export interface IBasicContentProvider { + id: string; + options: IAccessibleViewOptions; + onClose(): void; + provideContent(): string; + actions?: IAction[]; + previous?(): void; + next?(): void; +} diff --git a/src/vs/platform/accessibility/browser/accessibleViewRegistry.ts b/src/vs/platform/accessibility/browser/accessibleViewRegistry.ts new file mode 100644 index 00000000000..9390da5a534 --- /dev/null +++ b/src/vs/platform/accessibility/browser/accessibleViewRegistry.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable } from 'vs/base/common/lifecycle'; +import { AccessibleViewType, AdvancedContentProvider, ExtensionContentProvider } from 'vs/platform/accessibility/browser/accessibleView'; +import { ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; + +export interface IAccessibleViewImplentation { + type: AccessibleViewType; + priority: number; + name: string; + /** + * @returns the provider or undefined if the view should not be shown + */ + getProvider: (accessor: ServicesAccessor) => AdvancedContentProvider | ExtensionContentProvider | undefined; + when?: ContextKeyExpression | undefined; +} + +export const AccessibleViewRegistry = new class AccessibleViewRegistry { + _implementations: IAccessibleViewImplentation[] = []; + + register(implementation: IAccessibleViewImplentation): IDisposable { + this._implementations.push(implementation); + return { + dispose: () => { + const idx = this._implementations.indexOf(implementation); + if (idx !== -1) { + this._implementations.splice(idx, 1); + } + } + }; + } + + getImplementations(): IAccessibleViewImplentation[] { + return this._implementations; + } +}; + +export function alertAccessibleViewFocusChange(index: number | undefined, length: number | undefined, type: 'next' | 'previous'): void { + if (index === undefined || length === undefined) { + return; + } + const number = index + 1; + + if (type === 'next' && number + 1 <= length) { + alert(`Focused ${number + 1} of ${length}`); + } else if (type === 'previous' && number - 1 > 0) { + alert(`Focused ${number - 1} of ${length}`); + } + return; +} diff --git a/src/vs/workbench/browser/parts/notifications/notificationAccessibleView.ts b/src/vs/workbench/browser/parts/notifications/notificationAccessibleView.ts new file mode 100644 index 00000000000..066afd7adda --- /dev/null +++ b/src/vs/workbench/browser/parts/notifications/notificationAccessibleView.ts @@ -0,0 +1,133 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IAction } from 'vs/base/common/actions'; +import { Codicon } from 'vs/base/common/codicons'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { localize } from 'vs/nls'; +import { IAccessibleViewService, AccessibleViewProviderId, AccessibleViewType } from 'vs/platform/accessibility/browser/accessibleView'; +import { IAccessibleViewImplentation, alertAccessibleViewFocusChange } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { IAccessibilitySignalService, AccessibilitySignal } from 'vs/platform/accessibilitySignal/browser/accessibilitySignalService'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IListService, WorkbenchList } from 'vs/platform/list/browser/listService'; +import { getNotificationFromContext } from 'vs/workbench/browser/parts/notifications/notificationsCommands'; +import { NotificationFocusedContext } from 'vs/workbench/common/contextkeys'; +import { INotificationViewItem } from 'vs/workbench/common/notifications'; + +export class NotificationAccessibleView implements IAccessibleViewImplentation { + readonly priority = 90; + readonly name = 'notifications'; + readonly when = NotificationFocusedContext; + readonly type = AccessibleViewType.View; + getProvider(accessor: ServicesAccessor) { + const accessibleViewService = accessor.get(IAccessibleViewService); + const listService = accessor.get(IListService); + const commandService = accessor.get(ICommandService); + const accessibilitySignalService = accessor.get(IAccessibilitySignalService); + + function getProvider() { + const notification = getNotificationFromContext(listService); + if (!notification) { + return; + } + commandService.executeCommand('notifications.showList'); + let notificationIndex: number | undefined; + let length: number | undefined; + const list = listService.lastFocusedList; + if (list instanceof WorkbenchList) { + notificationIndex = list.indexOf(notification); + length = list.length; + } + if (notificationIndex === undefined) { + return; + } + + function focusList(): void { + commandService.executeCommand('notifications.showList'); + if (list && notificationIndex !== undefined) { + list.domFocus(); + try { + list.setFocus([notificationIndex]); + } catch { } + } + } + const message = notification.message.original.toString(); + if (!message) { + return; + } + notification.onDidClose(() => accessibleViewService.next()); + return { + id: AccessibleViewProviderId.Notification, + provideContent: () => { + return notification.source ? localize('notification.accessibleViewSrc', '{0} Source: {1}', message, notification.source) : localize('notification.accessibleView', '{0}', message); + }, + onClose(): void { + focusList(); + }, + next(): void { + if (!list) { + return; + } + focusList(); + list.focusNext(); + alertAccessibleViewFocusChange(notificationIndex, length, 'next'); + getProvider(); + }, + previous(): void { + if (!list) { + return; + } + focusList(); + list.focusPrevious(); + alertAccessibleViewFocusChange(notificationIndex, length, 'previous'); + getProvider(); + }, + verbositySettingKey: 'accessibility.verbosity.notification', + options: { type: AccessibleViewType.View }, + actions: getActionsFromNotification(notification, accessibilitySignalService) + }; + } + return getProvider(); + } +} + + +function getActionsFromNotification(notification: INotificationViewItem, accessibilitySignalService: IAccessibilitySignalService): IAction[] | undefined { + let actions = undefined; + if (notification.actions) { + actions = []; + if (notification.actions.primary) { + actions.push(...notification.actions.primary); + } + if (notification.actions.secondary) { + actions.push(...notification.actions.secondary); + } + } + if (actions) { + for (const action of actions) { + action.class = ThemeIcon.asClassName(Codicon.bell); + const initialAction = action.run; + action.run = () => { + initialAction(); + notification.close(); + }; + } + } + const manageExtension = actions?.find(a => a.label.includes('Manage Extension')); + if (manageExtension) { + manageExtension.class = ThemeIcon.asClassName(Codicon.gear); + } + if (actions) { + actions.push({ + id: 'clearNotification', label: localize('clearNotification', "Clear Notification"), tooltip: localize('clearNotification', "Clear Notification"), run: () => { + notification.close(); + accessibilitySignalService.playSignal(AccessibilitySignal.clear); + }, enabled: true, class: ThemeIcon.asClassName(Codicon.clearAll) + }); + } + return actions; +} + diff --git a/src/vs/workbench/browser/workbench.ts b/src/vs/workbench/browser/workbench.ts index 3678a7f6286..b0688133537 100644 --- a/src/vs/workbench/browser/workbench.ts +++ b/src/vs/workbench/browser/workbench.ts @@ -48,6 +48,8 @@ import { setHoverDelegateFactory } from 'vs/base/browser/ui/hover/hoverDelegateF import { setBaseLayerHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate2'; import { AccessibilityProgressSignalScheduler } from 'vs/platform/accessibilitySignal/browser/progressAccessibilitySignalScheduler'; import { setProgressAcccessibilitySignalScheduler } from 'vs/base/browser/ui/progressbar/progressAccessibilitySignal'; +import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { NotificationAccessibleView } from 'vs/workbench/browser/parts/notifications/notificationAccessibleView'; export interface IWorkbenchOptions { @@ -416,6 +418,9 @@ export class Workbench extends Layout { // Register Commands registerNotificationCommands(notificationsCenter, notificationsToasts, notificationService.model); + // Register notification accessible view + AccessibleViewRegistry.register(new NotificationAccessibleView()); + // Register with Layout this.registerNotifications({ onDidChangeNotificationsVisibility: Event.map(Event.any(notificationsToasts.onDidChangeVisibility, notificationsCenter.onDidChangeVisibility), () => notificationsToasts.isVisible || notificationsCenter.isVisible) diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts b/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts index aa516a28537..9a00c6035bf 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts @@ -8,16 +8,17 @@ import { DynamicSpeechAccessibilityConfiguration, registerAccessibilityConfigura import { IWorkbenchContributionsRegistry, WorkbenchPhase, Extensions as WorkbenchExtensions, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; -import { IAccessibleViewService, AccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; import { UnfocusedViewDimmingContribution } from 'vs/workbench/contrib/accessibility/browser/unfocusedViewDimmingContribution'; -import { ExtensionAccessibilityHelpDialogContribution, CommentAccessibleViewContribution, HoverAccessibleViewContribution, InlineCompletionsAccessibleViewContribution, NotificationAccessibleViewContribution } from 'vs/workbench/contrib/accessibility/browser/accessibleViewContributions'; import { AccessibilityStatus } from 'vs/workbench/contrib/accessibility/browser/accessibilityStatus'; import { EditorAccessibilityHelpContribution } from 'vs/workbench/contrib/accessibility/browser/editorAccessibilityHelp'; import { SaveAccessibilitySignalContribution } from 'vs/workbench/contrib/accessibilitySignals/browser/saveAccessibilitySignal'; -import { CommentsAccessibilityHelpContribution } from 'vs/workbench/contrib/comments/browser/commentsAccessibility'; import { DiffEditorActiveAnnouncementContribution } from 'vs/workbench/contrib/accessibilitySignals/browser/openDiffEditorAnnouncement'; import { SpeechAccessibilitySignalContribution } from 'vs/workbench/contrib/speech/browser/speechAccessibilitySignal'; import { AccessibleViewInformationService, IAccessibleViewInformationService } from 'vs/workbench/services/accessibility/common/accessibleViewInformationService'; +import { IAccessibleViewService } from 'vs/platform/accessibility/browser/accessibleView'; +import { AccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; +import { AccesibleViewHelpContribution, AccesibleViewContributions } from 'vs/workbench/contrib/accessibility/browser/accessibleViewContributions'; +import { ExtensionAccessibilityHelpDialogContribution } from 'vs/workbench/contrib/accessibility/browser/extensionAccesibilityHelp.contribution'; registerAccessibilityConfiguration(); registerSingleton(IAccessibleViewService, AccessibleViewService, InstantiationType.Delayed); @@ -25,13 +26,10 @@ registerSingleton(IAccessibleViewInformationService, AccessibleViewInformationSe const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchRegistry.registerWorkbenchContribution(EditorAccessibilityHelpContribution, LifecyclePhase.Eventually); -workbenchRegistry.registerWorkbenchContribution(CommentsAccessibilityHelpContribution, LifecyclePhase.Eventually); workbenchRegistry.registerWorkbenchContribution(UnfocusedViewDimmingContribution, LifecyclePhase.Restored); -workbenchRegistry.registerWorkbenchContribution(HoverAccessibleViewContribution, LifecyclePhase.Eventually); -workbenchRegistry.registerWorkbenchContribution(NotificationAccessibleViewContribution, LifecyclePhase.Eventually); -workbenchRegistry.registerWorkbenchContribution(CommentAccessibleViewContribution, LifecyclePhase.Eventually); -workbenchRegistry.registerWorkbenchContribution(InlineCompletionsAccessibleViewContribution, LifecyclePhase.Eventually); +workbenchRegistry.registerWorkbenchContribution(AccesibleViewHelpContribution, LifecyclePhase.Eventually); +workbenchRegistry.registerWorkbenchContribution(AccesibleViewContributions, LifecyclePhase.Eventually); registerWorkbenchContribution2(AccessibilityStatus.ID, AccessibilityStatus, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(ExtensionAccessibilityHelpDialogContribution.ID, ExtensionAccessibilityHelpDialogContribution, WorkbenchPhase.BlockRestore); diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts index ef8197fa3b0..381c77c96e8 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts @@ -59,23 +59,6 @@ export const enum AccessibilityVerbositySettingId { DiffEditorActive = 'accessibility.verbosity.diffEditorActive' } -export const enum AccessibleViewProviderId { - Terminal = 'terminal', - TerminalChat = 'terminal-chat', - TerminalHelp = 'terminal-help', - DiffEditor = 'diffEditor', - Chat = 'panelChat', - InlineChat = 'inlineChat', - InlineCompletions = 'inlineCompletions', - KeybindingsEditor = 'keybindingsEditor', - Notebook = 'notebook', - Editor = 'editor', - Hover = 'hover', - Notification = 'notification', - EmptyEditorHint = 'emptyEditorHint', - Comments = 'comments' -} - const baseVerbosityProperty: IConfigurationPropertySchema = { type: 'boolean', default: true, diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts index ffd923df3b3..dbba0da0420 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts @@ -9,7 +9,6 @@ import { ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; import { alert } from 'vs/base/browser/ui/aria/aria'; import { IAction } from 'vs/base/common/actions'; import { Codicon } from 'vs/base/common/codicons'; -import { Event } from 'vs/base/common/event'; import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { marked } from 'vs/base/common/marked/marked'; @@ -25,6 +24,7 @@ import { IModelService } from 'vs/editor/common/services/model'; import { AccessibilityHelpNLS } from 'vs/editor/common/standaloneStrings'; import { CodeActionController } from 'vs/editor/contrib/codeAction/browser/codeActionController'; import { localize } from 'vs/nls'; +import { AccessibleViewProviderId, AccessibleViewType, AdvancedContentProvider, ExtensionContentProvider, IAccessibleViewService, IAccessibleViewSymbol } from 'vs/platform/accessibility/browser/accessibleView'; import { ACCESSIBLE_VIEW_SHOWN_STORAGE_PREFIX, IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { WorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; @@ -33,15 +33,14 @@ import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextViewDelegate, IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { IInstantiationService, createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ResultKind } from 'vs/platform/keybinding/common/keybindingResolver'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { IPickerQuickAccessItem } from 'vs/platform/quickinput/browser/pickerQuickAccess'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { AccessibilityVerbositySettingId, AccessibilityWorkbenchSettingId, AccessibleViewProviderId, accessibilityHelpIsShown, accessibleViewContainsCodeBlocks, accessibleViewCurrentProviderId, accessibleViewGoToSymbolSupported, accessibleViewInCodeBlock, accessibleViewIsShown, accessibleViewOnLastLine, accessibleViewSupportsNavigation, accessibleViewVerbosityEnabled } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { AccessibilityVerbositySettingId, AccessibilityWorkbenchSettingId, accessibilityHelpIsShown, accessibleViewContainsCodeBlocks, accessibleViewCurrentProviderId, accessibleViewGoToSymbolSupported, accessibleViewInCodeBlock, accessibleViewIsShown, accessibleViewOnLastLine, accessibleViewSupportsNavigation, accessibleViewVerbosityEnabled } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; import { IChatCodeBlockContextProviderService } from 'vs/workbench/contrib/chat/browser/chat'; import { ICodeBlockActionContext } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; @@ -51,122 +50,7 @@ const enum DIMENSIONS { MAX_WIDTH = 600 } -type ContentProvider = AdvancedContentProvider | ExtensionContentProvider; - -export class AdvancedContentProvider implements IAccessibleViewContentProvider { - - constructor( - public id: AccessibleViewProviderId, - public options: IAccessibleViewOptions, - public provideContent: () => string, - public onClose: () => void, - public verbositySettingKey: AccessibilityVerbositySettingId, - public actions?: IAction[], - public next?: () => void, - public previous?: () => void, - public onKeyDown?: (e: IKeyboardEvent) => void, - public getSymbols?: () => IAccessibleViewSymbol[], - public onDidRequestClearLastProvider?: Event, - ) { } -} - -export class ExtensionContentProvider implements IBasicContentProvider { - - constructor( - public readonly id: string, - public options: IAccessibleViewOptions, - public provideContent: () => string, - public onClose: () => void, - public next?: () => void, - public previous?: () => void, - public actions?: IAction[], - ) { } -} - -export interface IBasicContentProvider { - id: string; - options: IAccessibleViewOptions; - onClose(): void; - provideContent(): string; - actions?: IAction[]; - previous?(): void; - next?(): void; -} - -export interface IAccessibleViewContentProvider extends IBasicContentProvider { - id: AccessibleViewProviderId; - verbositySettingKey: AccessibilityVerbositySettingId; - /** - * Note that a Codicon class should be provided for each action. - * If not, a default will be used. - */ - onKeyDown?(e: IKeyboardEvent): void; - /** - * When the language is markdown, this is provided by default. - */ - getSymbols?(): IAccessibleViewSymbol[]; - /** - * Note that this will only take effect if the provider has an ID. - */ - onDidRequestClearLastProvider?: Event; -} - -export const IAccessibleViewService = createDecorator('accessibleViewService'); - -export interface IAccessibleViewService { - readonly _serviceBrand: undefined; - show(provider: ContentProvider, position?: Position): void; - showLastProvider(id: AccessibleViewProviderId): void; - showAccessibleViewHelp(): void; - next(): void; - previous(): void; - navigateToCodeBlock(type: 'next' | 'previous'): void; - goToSymbol(): void; - disableHint(): void; - getPosition(id: AccessibleViewProviderId): Position | undefined; - setPosition(position: Position, reveal?: boolean): void; - getLastPosition(): Position | undefined; - /** - * If the setting is enabled, provides the open accessible view hint as a localized string. - * @param verbositySettingKey The setting key for the verbosity of the feature - */ - getOpenAriaHint(verbositySettingKey: AccessibilityVerbositySettingId): string | null; - getCodeBlockContext(): ICodeBlockActionContext | undefined; -} - -export const enum AccessibleViewType { - Help = 'help', - View = 'view' -} - -export const enum NavigationType { - Previous = 'previous', - Next = 'next' -} - -export interface IAccessibleViewOptions { - readMoreUrl?: string; - /** - * Defaults to markdown - */ - language?: string; - type: AccessibleViewType; - /** - * By default, places the cursor on the top line of the accessible view. - * If set to 'initial-bottom', places the cursor on the bottom line of the accessible view and preserves it henceforth. - * If set to 'bottom', places the cursor on the bottom line of the accessible view. - */ - position?: 'bottom' | 'initial-bottom'; - /** - * @returns a string that will be used as the content of the help dialog - * instead of the one provided by default. - */ - customHelp?: () => string; - /** - * If this provider might want to request to be shown again, provide an ID. - */ - id?: AccessibleViewProviderId; -} +export type AccesibleViewContentProvider = AdvancedContentProvider | ExtensionContentProvider; interface ICodeBlock { startLine: number; @@ -194,10 +78,10 @@ export class AccessibleView extends Disposable { private _title: HTMLElement; private readonly _toolbar: WorkbenchToolBar; - private _currentProvider: ContentProvider | undefined; + private _currentProvider: AccesibleViewContentProvider | undefined; private _currentContent: string | undefined; - private _lastProvider: ContentProvider | undefined; + private _lastProvider: AccesibleViewContentProvider | undefined; constructor( @IOpenerService private readonly _openerService: IOpenerService, @@ -354,7 +238,7 @@ export class AccessibleView extends Disposable { this.show(this._lastProvider); } - show(provider?: ContentProvider, symbol?: IAccessibleViewSymbol, showAccessibleViewHelp?: boolean, position?: Position): void { + show(provider?: AccesibleViewContentProvider, symbol?: IAccessibleViewSymbol, showAccessibleViewHelp?: boolean, position?: Position): void { provider = provider ?? this._currentProvider; if (!provider) { return; @@ -506,7 +390,7 @@ export class AccessibleView extends Disposable { } } - showSymbol(provider: ContentProvider, symbol: IAccessibleViewSymbol): void { + showSymbol(provider: AccesibleViewContentProvider, symbol: IAccessibleViewSymbol): void { if (!this._currentContent) { return; } @@ -540,7 +424,7 @@ export class AccessibleView extends Disposable { alert(localize('disableAccessibilityHelp', '{0} accessibility verbosity is now disabled', this._currentProvider.verbositySettingKey)); } - private _updateContextKeys(provider: ContentProvider, shown: boolean): void { + private _updateContextKeys(provider: AccesibleViewContentProvider, shown: boolean): void { if (provider.options.type === AccessibleViewType.Help) { this._accessiblityHelpIsShown.set(shown); this._accessibleViewIsShown.reset(); @@ -553,7 +437,7 @@ export class AccessibleView extends Disposable { this._accessibleViewGoToSymbolSupported.set(this._goToSymbolsSupported() ? this.getSymbols()?.length! > 0 : false); } - private _render(provider: ContentProvider, container: HTMLElement, showAccessibleViewHelp?: boolean): IDisposable { + private _render(provider: AccesibleViewContentProvider, container: HTMLElement, showAccessibleViewHelp?: boolean): IDisposable { this._currentProvider = provider; this._accessibleViewCurrentProviderId.set(provider.id); const verbose = this._verbosityEnabled(); @@ -709,7 +593,7 @@ export class AccessibleView extends Disposable { return this._currentProvider.options.type === AccessibleViewType.Help || this._currentProvider.options.language === 'markdown' || this._currentProvider.options.language === undefined || (this._currentProvider instanceof AdvancedContentProvider && !!this._currentProvider.getSymbols?.()); } - private _updateLastProvider(): ContentProvider | undefined { + private _updateLastProvider(): AccesibleViewContentProvider | undefined { const provider = this._currentProvider; if (!provider) { return; @@ -820,7 +704,7 @@ export class AccessibleView extends Disposable { } return hint; } - private _getDisableVerbosityHint(verbositySettingKey: AccessibilityVerbositySettingId): string { + private _getDisableVerbosityHint(verbositySettingKey: AccessibilityVerbositySettingId | string): string { if (!this._configurationService.getValue(verbositySettingKey)) { return ''; } @@ -860,7 +744,7 @@ export class AccessibleViewService extends Disposable implements IAccessibleView super(); } - show(provider: ContentProvider, position?: Position): void { + show(provider: AccesibleViewContentProvider, position?: Position): void { if (!this._accessibleView) { this._accessibleView = this._register(this._instantiationService.createInstance(AccessibleView)); } @@ -923,7 +807,7 @@ class AccessibleViewSymbolQuickPick { constructor(private _accessibleView: AccessibleView, @IQuickInputService private readonly _quickInputService: IQuickInputService) { } - show(provider: ContentProvider): void { + show(provider: AccesibleViewContentProvider): void { const quickPick = this._quickInputService.createQuickPick(); quickPick.placeholder = localize('accessibleViewSymbolQuickPickPlaceholder', "Type to search symbols"); quickPick.title = localize('accessibleViewSymbolQuickPickTitle', "Go to Symbol Accessible View"); @@ -954,12 +838,6 @@ class AccessibleViewSymbolQuickPick { } } -export interface IAccessibleViewSymbol extends IPickerQuickAccessItem { - markdownToParse?: string; - firstListItem?: string; - lineNumber?: number; - endLineNumber?: number; -} function shouldHide(event: KeyboardEvent, keybindingService: IKeybindingService, configurationService: IConfigurationService): boolean { if (!configurationService.getValue(AccessibilityWorkbenchSettingId.AccessibleViewCloseOnKeyPress)) { diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts index d83fb4e7365..0284884cab2 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts @@ -11,8 +11,8 @@ import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/act import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; -import { AccessibleViewProviderId, accessibilityHelpIsShown, accessibleViewContainsCodeBlocks, accessibleViewCurrentProviderId, accessibleViewGoToSymbolSupported, accessibleViewIsShown, accessibleViewSupportsNavigation, accessibleViewVerbosityEnabled } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; +import { accessibilityHelpIsShown, accessibleViewContainsCodeBlocks, accessibleViewCurrentProviderId, accessibleViewGoToSymbolSupported, accessibleViewIsShown, accessibleViewSupportsNavigation, accessibleViewVerbosityEnabled } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { AccessibleViewProviderId, IAccessibleViewService } from 'vs/platform/accessibility/browser/accessibleView'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { InlineCompletionsController } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController'; diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleViewContributions.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleViewContributions.ts index c01a8f828ea..73b4e7ce169 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleViewContributions.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleViewContributions.ts @@ -3,45 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Disposable, DisposableMap, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { localize } from 'vs/nls'; +import { Disposable } from 'vs/base/common/lifecycle'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { AccessibilityVerbositySettingId, AccessibleViewProviderId, accessibleViewIsShown } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { accessibleViewIsShown } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import * as strings from 'vs/base/common/strings'; -import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { getNotificationFromContext } from 'vs/workbench/browser/parts/notifications/notificationsCommands'; -import { IListService, WorkbenchList } from 'vs/platform/list/browser/listService'; -import { FocusedViewContext, NotificationFocusedContext } from 'vs/workbench/common/contextkeys'; -import { IAccessibleViewService, IAccessibleViewOptions, AccessibleViewType, ExtensionContentProvider } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; -import { IHoverService } from 'vs/platform/hover/browser/hover'; -import { alert } from 'vs/base/browser/ui/aria/aria'; import { AccessibilityHelpAction, AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; -import { IAction } from 'vs/base/common/actions'; -import { INotificationViewItem } from 'vs/workbench/common/notifications'; -import { ThemeIcon } from 'vs/base/common/themables'; -import { Codicon } from 'vs/base/common/codicons'; -import { InlineCompletionsController } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController'; -import { InlineCompletionContextKeys } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionContextKeys'; -import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { AccessibilitySignal, IAccessibilitySignalService } from 'vs/platform/accessibilitySignal/browser/accessibilitySignalService'; -import { Extensions, IViewDescriptor, IViewsRegistry } from 'vs/workbench/common/views'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { COMMENTS_VIEW_ID, CommentsMenus } from 'vs/workbench/contrib/comments/browser/commentsTreeViewer'; -import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; -import { CommentsPanel, CONTEXT_KEY_HAS_COMMENTS } from 'vs/workbench/contrib/comments/browser/commentsView'; -import { IMenuService } from 'vs/platform/actions/common/actions'; -import { MarshalledId } from 'vs/base/common/marshallingIds'; -import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; -import { MarkdownString } from 'vs/base/common/htmlContent'; -import { URI } from 'vs/base/common/uri'; +import { AccessibleViewType, AdvancedContentProvider, ExtensionContentProvider, IAccessibleViewService } from 'vs/platform/accessibility/browser/accessibleView'; +import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; export function descriptionForCommand(commandId: string, msg: string, noKbMsg: string, keybindingService: IKeybindingService): string { const kb = keybindingService.lookupKeybinding(commandId); @@ -51,377 +20,36 @@ export function descriptionForCommand(commandId: string, msg: string, noKbMsg: s return strings.format(noKbMsg, commandId); } -export class HoverAccessibleViewContribution extends Disposable { - static ID: 'hoverAccessibleViewContribution'; - private _options: IAccessibleViewOptions = { language: 'typescript', type: AccessibleViewType.View }; + +export class AccesibleViewHelpContribution extends Disposable { + static ID: 'accesibleViewHelpContribution'; constructor() { super(); - this._register(AccessibleViewAction.addImplementation(95, 'hover', accessor => { - const accessibleViewService = accessor.get(IAccessibleViewService); - const codeEditorService = accessor.get(ICodeEditorService); - const editor = codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor(); - const editorHoverContent = editor ? HoverController.get(editor)?.getWidgetContent() ?? undefined : undefined; - if (!editor || !editorHoverContent) { - return false; - } - this._options.language = editor?.getModel()?.getLanguageId() ?? undefined; - accessibleViewService.show({ - id: AccessibleViewProviderId.Hover, - verbositySettingKey: AccessibilityVerbositySettingId.Hover, - provideContent() { return editorHoverContent; }, - onClose() { - HoverController.get(editor)?.focus(); - }, - options: this._options - }); - return true; - }, EditorContextKeys.hoverFocused)); - this._register(AccessibleViewAction.addImplementation(90, 'extension-hover', accessor => { - const accessibleViewService = accessor.get(IAccessibleViewService); - const contextViewService = accessor.get(IContextViewService); - const contextViewElement = contextViewService.getContextViewElement(); - const extensionHoverContent = contextViewElement?.textContent ?? undefined; - const hoverService = accessor.get(IHoverService); - - if (contextViewElement.classList.contains('accessible-view-container') || !extensionHoverContent) { - // The accessible view, itself, uses the context view service to display the text. We don't want to read that. - return false; - } - accessibleViewService.show({ - id: AccessibleViewProviderId.Hover, - verbositySettingKey: AccessibilityVerbositySettingId.Hover, - provideContent() { return extensionHoverContent; }, - onClose() { - hoverService.showAndFocusLastHover(); - }, - options: this._options - }); - return true; - })); - this._register(AccessibilityHelpAction.addImplementation(115, 'accessible-view', accessor => { + this._register(AccessibilityHelpAction.addImplementation(115, 'accessible-view-help', accessor => { accessor.get(IAccessibleViewService).showAccessibleViewHelp(); return true; }, accessibleViewIsShown)); } } -export class NotificationAccessibleViewContribution extends Disposable { - static ID: 'notificationAccessibleViewContribution'; +export class AccesibleViewContributions extends Disposable { + static ID: 'accesibleViewContributions'; constructor() { super(); - this._register(AccessibleViewAction.addImplementation(90, 'notifications', accessor => { - const accessibleViewService = accessor.get(IAccessibleViewService); - const listService = accessor.get(IListService); - const commandService = accessor.get(ICommandService); - const accessibilitySignalService = accessor.get(IAccessibilitySignalService); - - function renderAccessibleView(): boolean { - const notification = getNotificationFromContext(listService); - if (!notification) { - return false; + AccessibleViewRegistry.getImplementations().forEach(impl => { + const implementation = (accessor: ServicesAccessor) => { + const provider: AdvancedContentProvider | ExtensionContentProvider | undefined = impl.getProvider(accessor); + if (provider) { + accessor.get(IAccessibleViewService).show(provider); + return true; } - commandService.executeCommand('notifications.showList'); - let notificationIndex: number | undefined; - let length: number | undefined; - const list = listService.lastFocusedList; - if (list instanceof WorkbenchList) { - notificationIndex = list.indexOf(notification); - length = list.length; - } - if (notificationIndex === undefined) { - return false; - } - - function focusList(): void { - commandService.executeCommand('notifications.showList'); - if (list && notificationIndex !== undefined) { - list.domFocus(); - try { - list.setFocus([notificationIndex]); - } catch { } - } - } - const message = notification.message.original.toString(); - if (!message) { - return false; - } - notification.onDidClose(() => accessibleViewService.next()); - accessibleViewService.show({ - id: AccessibleViewProviderId.Notification, - provideContent: () => { - return notification.source ? localize('notification.accessibleViewSrc', '{0} Source: {1}', message, notification.source) : localize('notification.accessibleView', '{0}', message); - }, - onClose(): void { - focusList(); - }, - next(): void { - if (!list) { - return; - } - focusList(); - list.focusNext(); - alertFocusChange(notificationIndex, length, 'next'); - renderAccessibleView(); - }, - previous(): void { - if (!list) { - return; - } - focusList(); - list.focusPrevious(); - alertFocusChange(notificationIndex, length, 'previous'); - renderAccessibleView(); - }, - verbositySettingKey: AccessibilityVerbositySettingId.Notification, - options: { type: AccessibleViewType.View }, - actions: getActionsFromNotification(notification, accessibilitySignalService) - }); - return true; - } - return renderAccessibleView(); - }, NotificationFocusedContext)); - } -} - -function getActionsFromNotification(notification: INotificationViewItem, accessibilitySignalService: IAccessibilitySignalService): IAction[] | undefined { - let actions = undefined; - if (notification.actions) { - actions = []; - if (notification.actions.primary) { - actions.push(...notification.actions.primary); - } - if (notification.actions.secondary) { - actions.push(...notification.actions.secondary); - } - } - if (actions) { - for (const action of actions) { - action.class = ThemeIcon.asClassName(Codicon.bell); - const initialAction = action.run; - action.run = () => { - initialAction(); - notification.close(); + return false; }; - } - } - const manageExtension = actions?.find(a => a.label.includes('Manage Extension')); - if (manageExtension) { - manageExtension.class = ThemeIcon.asClassName(Codicon.gear); - } - if (actions) { - actions.push({ - id: 'clearNotification', label: localize('clearNotification', "Clear Notification"), tooltip: localize('clearNotification', "Clear Notification"), run: () => { - notification.close(); - accessibilitySignalService.playSignal(AccessibilitySignal.clear); - }, enabled: true, class: ThemeIcon.asClassName(Codicon.clearAll) + if (impl.type === AccessibleViewType.View) { + this._register(AccessibleViewAction.addImplementation(impl.priority, impl.name, implementation, impl.when)); + } else { + this._register(AccessibilityHelpAction.addImplementation(impl.priority, impl.name, implementation, impl.when)); + } }); } - return actions; -} - -export function alertFocusChange(index: number | undefined, length: number | undefined, type: 'next' | 'previous'): void { - if (index === undefined || length === undefined) { - return; - } - const number = index + 1; - - if (type === 'next' && number + 1 <= length) { - alert(`Focused ${number + 1} of ${length}`); - } else if (type === 'previous' && number - 1 > 0) { - alert(`Focused ${number - 1} of ${length}`); - } - return; -} - - -export class CommentAccessibleViewContribution extends Disposable { - static ID: 'commentAccessibleViewContribution'; - constructor() { - super(); - this._register(AccessibleViewAction.addImplementation(90, 'comment', accessor => { - const accessibleViewService = accessor.get(IAccessibleViewService); - const contextKeyService = accessor.get(IContextKeyService); - const viewsService = accessor.get(IViewsService); - const menuService = accessor.get(IMenuService); - const commentsView = viewsService.getActiveViewWithId(COMMENTS_VIEW_ID); - if (!commentsView) { - return false; - } - const menus = this._register(new CommentsMenus(menuService)); - menus.setContextKeyService(contextKeyService); - - function renderAccessibleView() { - if (!commentsView) { - return false; - } - - const commentNode = commentsView.focusedCommentNode; - const content = commentsView.focusedCommentInfo?.toString(); - if (!commentNode || !content) { - return false; - } - const menuActions = [...menus.getResourceContextActions(commentNode)].filter(i => i.enabled); - const actions = menuActions.map(action => { - return { - ...action, - run: () => { - commentsView.focus(); - action.run({ - thread: commentNode.thread, - $mid: MarshalledId.CommentThread, - commentControlHandle: commentNode.controllerHandle, - commentThreadHandle: commentNode.threadHandle, - }); - } - }; - }); - accessibleViewService.show({ - id: AccessibleViewProviderId.Notification, - provideContent: () => { - return content; - }, - onClose(): void { - commentsView.focus(); - }, - next(): void { - commentsView.focus(); - commentsView.focusNextNode(); - renderAccessibleView(); - }, - previous(): void { - commentsView.focus(); - commentsView.focusPreviousNode(); - renderAccessibleView(); - }, - verbositySettingKey: AccessibilityVerbositySettingId.Comments, - options: { type: AccessibleViewType.View }, - actions - }); - return true; - } - return renderAccessibleView(); - }, CONTEXT_KEY_HAS_COMMENTS)); - } -} - -export class InlineCompletionsAccessibleViewContribution extends Disposable { - static ID: 'inlineCompletionsAccessibleViewContribution'; - private _options: IAccessibleViewOptions = { type: AccessibleViewType.View }; - constructor() { - super(); - this._register(AccessibleViewAction.addImplementation(95, 'inline-completions', accessor => { - const accessibleViewService = accessor.get(IAccessibleViewService); - const codeEditorService = accessor.get(ICodeEditorService); - const show = () => { - const editor = codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor(); - if (!editor) { - return false; - } - const model = InlineCompletionsController.get(editor)?.model.get(); - const state = model?.state.get(); - if (!model || !state) { - return false; - } - const lineText = model.textModel.getLineContent(state.primaryGhostText.lineNumber); - const ghostText = state.primaryGhostText.renderForScreenReader(lineText); - if (!ghostText) { - return false; - } - this._options.language = editor.getModel()?.getLanguageId() ?? undefined; - accessibleViewService.show({ - id: AccessibleViewProviderId.InlineCompletions, - verbositySettingKey: AccessibilityVerbositySettingId.InlineCompletions, - provideContent() { return lineText + ghostText; }, - onClose() { - model.stop(); - editor.focus(); - }, - next() { - model.next(); - setTimeout(() => show(), 50); - }, - previous() { - model.previous(); - setTimeout(() => show(), 50); - }, - options: this._options - }); - return true; - }; ContextKeyExpr.and(InlineCompletionContextKeys.inlineSuggestionVisible); - return show(); - })); - } -} - -export class ExtensionAccessibilityHelpDialogContribution extends Disposable { - static ID = 'extensionAccessibilityHelpDialogContribution'; - private _viewHelpDialogMap = this._register(new DisposableMap()); - constructor(@IKeybindingService keybindingService: IKeybindingService) { - super(); - this._register(Registry.as(Extensions.ViewsRegistry).onViewsRegistered(e => { - for (const view of e) { - for (const viewDescriptor of view.views) { - if (viewDescriptor.accessibilityHelpContent) { - this._viewHelpDialogMap.set(viewDescriptor.id, registerAccessibilityHelpAction(keybindingService, viewDescriptor)); - } - } - } - })); - this._register(Registry.as(Extensions.ViewsRegistry).onViewsDeregistered(e => { - for (const viewDescriptor of e.views) { - if (viewDescriptor.accessibilityHelpContent) { - this._viewHelpDialogMap.get(viewDescriptor.id)?.dispose(); - } - } - })); - } -} - -function registerAccessibilityHelpAction(keybindingService: IKeybindingService, viewDescriptor: IViewDescriptor): IDisposable { - const disposableStore = new DisposableStore(); - const helpContent = resolveExtensionHelpContent(keybindingService, viewDescriptor.accessibilityHelpContent); - if (!helpContent) { - throw new Error('No help content for view'); - } - disposableStore.add(AccessibilityHelpAction.addImplementation(95, viewDescriptor.id, accessor => { - const accessibleViewService = accessor.get(IAccessibleViewService); - const viewsService = accessor.get(IViewsService); - accessibleViewService.show(new ExtensionContentProvider( - viewDescriptor.id, - { type: AccessibleViewType.Help }, - () => helpContent.value, - () => viewsService.openView(viewDescriptor.id, true) - )); - return true; - }, FocusedViewContext.isEqualTo(viewDescriptor.id))); - disposableStore.add(keybindingService.onDidUpdateKeybindings(() => { - disposableStore.clear(); - disposableStore.add(registerAccessibilityHelpAction(keybindingService, viewDescriptor)); - })); - return disposableStore; -} - -function resolveExtensionHelpContent(keybindingService: IKeybindingService, content?: MarkdownString): MarkdownString | undefined { - if (!content) { - return; - } - let resolvedContent = typeof content === 'string' ? content : content.value; - const matches = resolvedContent.matchAll(/\.*)\>/gm); - for (const match of [...matches]) { - const commandId = match?.groups?.commandId; - if (match?.length && commandId) { - const keybinding = keybindingService.lookupKeybinding(commandId)?.getAriaLabel(); - let kbLabel = keybinding; - if (!kbLabel) { - const args = URI.parse(`command:workbench.action.openGlobalKeybindings?${encodeURIComponent(JSON.stringify(commandId))}`); - kbLabel = ` [Configure a keybinding](${args})`; - } else { - kbLabel = ' (' + keybinding + ')'; - } - resolvedContent = resolvedContent.replace(match[0], kbLabel); - } - } - const result = new MarkdownString(resolvedContent); - result.isTrusted = true; - return result; } diff --git a/src/vs/workbench/contrib/accessibility/browser/editorAccessibilityHelp.ts b/src/vs/workbench/contrib/accessibility/browser/editorAccessibilityHelp.ts index 541b07a6bce..11b80cdbacb 100644 --- a/src/vs/workbench/contrib/accessibility/browser/editorAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/accessibility/browser/editorAccessibilityHelp.ts @@ -7,28 +7,27 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; -import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { AccessibilityHelpNLS } from 'vs/editor/common/standaloneStrings'; import { ToggleTabFocusModeAction } from 'vs/editor/contrib/toggleTabFocusMode/browser/toggleTabFocusMode'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { AccessibleViewProviderId, AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { descriptionForCommand } from 'vs/workbench/contrib/accessibility/browser/accessibleViewContributions'; -import { IAccessibleViewService, IAccessibleViewContentProvider, IAccessibleViewOptions, AccessibleViewType } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; import { AccessibilityHelpAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; import { CONTEXT_CHAT_ENABLED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { CommentAccessibilityHelpNLS } from 'vs/workbench/contrib/comments/browser/commentsAccessibility'; import { CommentCommandId } from 'vs/workbench/contrib/comments/common/commentCommandIds'; import { CommentContextKeys } from 'vs/workbench/contrib/comments/common/commentContextKeys'; import { NEW_UNTITLED_FILE_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileConstants'; +import { IAccessibleViewService, IAccessibleViewContentProvider, AccessibleViewProviderId, IAccessibleViewOptions, AccessibleViewType } from 'vs/platform/accessibility/browser/accessibleView'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; export class EditorAccessibilityHelpContribution extends Disposable { static ID: 'editorAccessibilityHelpContribution'; constructor() { super(); - this._register(AccessibilityHelpAction.addImplementation(95, 'editor', async accessor => { + this._register(AccessibilityHelpAction.addImplementation(90, 'editor', async accessor => { const codeEditorService = accessor.get(ICodeEditorService); const accessibleViewService = accessor.get(IAccessibleViewService); const instantiationService = accessor.get(IInstantiationService); @@ -39,7 +38,7 @@ export class EditorAccessibilityHelpContribution extends Disposable { codeEditor = codeEditorService.getActiveCodeEditor()!; } accessibleViewService.show(instantiationService.createInstance(EditorAccessibilityHelpProvider, codeEditor)); - }, EditorContextKeys.focus)); + })); } } diff --git a/src/vs/workbench/contrib/accessibility/browser/extensionAccesibilityHelp.contribution.ts b/src/vs/workbench/contrib/accessibility/browser/extensionAccesibilityHelp.contribution.ts new file mode 100644 index 00000000000..6161c11f1cc --- /dev/null +++ b/src/vs/workbench/contrib/accessibility/browser/extensionAccesibilityHelp.contribution.ts @@ -0,0 +1,94 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { MarkdownString } from 'vs/base/common/htmlContent'; +import { DisposableMap, IDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { AccessibleViewType, ExtensionContentProvider } from 'vs/platform/accessibility/browser/accessibleView'; +import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { FocusedViewContext } from 'vs/workbench/common/contextkeys'; +import { IViewsRegistry, Extensions, IViewDescriptor } from 'vs/workbench/common/views'; +import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; + +export class ExtensionAccessibilityHelpDialogContribution extends Disposable { + static ID = 'extensionAccessibilityHelpDialogContribution'; + private _viewHelpDialogMap = this._register(new DisposableMap()); + constructor(@IKeybindingService keybindingService: IKeybindingService) { + super(); + this._register(Registry.as(Extensions.ViewsRegistry).onViewsRegistered(e => { + for (const view of e) { + for (const viewDescriptor of view.views) { + if (viewDescriptor.accessibilityHelpContent) { + this._viewHelpDialogMap.set(viewDescriptor.id, registerAccessibilityHelpAction(keybindingService, viewDescriptor)); + } + } + } + })); + this._register(Registry.as(Extensions.ViewsRegistry).onViewsDeregistered(e => { + for (const viewDescriptor of e.views) { + if (viewDescriptor.accessibilityHelpContent) { + this._viewHelpDialogMap.get(viewDescriptor.id)?.dispose(); + } + } + })); + } +} + +function registerAccessibilityHelpAction(keybindingService: IKeybindingService, viewDescriptor: IViewDescriptor): IDisposable { + const disposableStore = new DisposableStore(); + const helpContent = resolveExtensionHelpContent(keybindingService, viewDescriptor.accessibilityHelpContent); + if (!helpContent) { + throw new Error('No help content for view'); + } + disposableStore.add(AccessibleViewRegistry.register({ + priority: 95, + name: viewDescriptor.id, + type: AccessibleViewType.Help, + when: FocusedViewContext.isEqualTo(viewDescriptor.id), + getProvider: (accessor: ServicesAccessor) => { + const viewsService = accessor.get(IViewsService); + return new ExtensionContentProvider( + viewDescriptor.id, + { type: AccessibleViewType.Help }, + () => helpContent.value, + () => viewsService.openView(viewDescriptor.id, true) + ); + } + })); + + disposableStore.add(keybindingService.onDidUpdateKeybindings(() => { + disposableStore.clear(); + disposableStore.add(registerAccessibilityHelpAction(keybindingService, viewDescriptor)); + })); + return disposableStore; +} + +function resolveExtensionHelpContent(keybindingService: IKeybindingService, content?: MarkdownString): MarkdownString | undefined { + if (!content) { + return; + } + let resolvedContent = typeof content === 'string' ? content : content.value; + const matches = resolvedContent.matchAll(/\.*)\>/gm); + for (const match of [...matches]) { + const commandId = match?.groups?.commandId; + if (match?.length && commandId) { + const keybinding = keybindingService.lookupKeybinding(commandId)?.getAriaLabel(); + let kbLabel = keybinding; + if (!kbLabel) { + const args = URI.parse(`command:workbench.action.openGlobalKeybindings?${encodeURIComponent(JSON.stringify(commandId))}`); + kbLabel = ` [Configure a keybinding](${args})`; + } else { + kbLabel = ' (' + keybinding + ')'; + } + resolvedContent = resolvedContent.replace(match[0], kbLabel); + } + } + const result = new MarkdownString(resolvedContent); + result.isTrusted = true; + return result; +} diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts index e225605c017..bd6d4053c81 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts @@ -9,10 +9,25 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; -import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; -import { AccessibilityVerbositySettingId, AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { AccessibleViewProviderId, AccessibleViewType } from 'vs/platform/accessibility/browser/accessibleView'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { AccessibleDiffViewerNext } from 'vs/editor/browser/widget/diffEditor/commands'; import { INLINE_CHAT_ID } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { CONTEXT_IN_CHAT_SESSION, CONTEXT_RESPONSE, CONTEXT_REQUEST } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; + +export class ChatAccessibilityHelp implements IAccessibleViewImplentation { + readonly priority = 105; + readonly name = 'panelChat'; + readonly type = AccessibleViewType.Help; + readonly when = ContextKeyExpr.or(CONTEXT_IN_CHAT_SESSION, CONTEXT_RESPONSE, CONTEXT_REQUEST); + getProvider(accessor: ServicesAccessor) { + const codeEditor = accessor.get(ICodeEditorService).getActiveCodeEditor() || accessor.get(ICodeEditorService).getFocusedCodeEditor(); + return getChatAccessibilityHelpProvider(accessor, codeEditor ?? undefined, 'panelChat'); + } +} export function getAccessibilityHelpText(accessor: ServicesAccessor, type: 'panelChat' | 'inlineChat'): string { const keybindingService = accessor.get(IKeybindingService); @@ -57,9 +72,8 @@ function descriptionForCommand(commandId: string, msg: string, noKbMsg: string, return format(noKbMsg, commandId); } -export async function runAccessibilityHelpAction(accessor: ServicesAccessor, editor: ICodeEditor | undefined, type: 'panelChat' | 'inlineChat'): Promise { +export function getChatAccessibilityHelpProvider(accessor: ServicesAccessor, editor: ICodeEditor | undefined, type: 'panelChat' | 'inlineChat') { const widgetService = accessor.get(IChatWidgetService); - const accessibleViewService = accessor.get(IAccessibleViewService); const inputEditor: ICodeEditor | undefined = type === 'panelChat' ? widgetService.lastFocusedWidget?.inputEditor : editor; if (!inputEditor) { @@ -73,7 +87,7 @@ export async function runAccessibilityHelpAction(accessor: ServicesAccessor, edi const cachedPosition = inputEditor.getPosition(); inputEditor.getSupportedActions(); const helpText = getAccessibilityHelpText(accessor, type); - accessibleViewService.show({ + return { id: type === 'panelChat' ? AccessibleViewProviderId.Chat : AccessibleViewProviderId.InlineChat, verbositySettingKey: type === 'panelChat' ? AccessibilityVerbositySettingId.Chat : AccessibilityVerbositySettingId.InlineChat, provideContent: () => helpText, @@ -90,5 +104,5 @@ export async function runAccessibilityHelpAction(accessor: ServicesAccessor, edi } }, options: { type: AccessibleViewType.Help } - }); + }; } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index a85f6e9c47d..7545ae0ccdd 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -5,32 +5,25 @@ import { Codicon } from 'vs/base/common/codicons'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { Disposable } from 'vs/base/common/lifecycle'; import { ThemeIcon } from 'vs/base/common/themables'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction2, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { localize, localize2 } from 'vs/nls'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IsLinuxContext, IsWindowsContext } from 'vs/platform/contextkey/common/contextkeys'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; -import { Registry } from 'vs/platform/registry/common/platform'; import { ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; -import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; -import { AccessibilityHelpAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; -import { runAccessibilityHelpAction } from 'vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp'; import { CHAT_VIEW_ID, IChatWidgetService, showChatView } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatEditor'; import { ChatEditorInput } from 'vs/workbench/contrib/chat/browser/chatEditorInput'; import { ChatViewPane } from 'vs/workbench/contrib/chat/browser/chatViewPane'; import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { CONTEXT_CHAT_INPUT_CURSOR_AT_TOP, CONTEXT_CHAT_LOCATION, CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION, CONTEXT_CHAT_ENABLED, CONTEXT_REQUEST, CONTEXT_RESPONSE } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { CONTEXT_CHAT_INPUT_CURSOR_AT_TOP, CONTEXT_CHAT_LOCATION, CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION, CONTEXT_CHAT_ENABLED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatDetail, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatWidgetHistoryService } from 'vs/workbench/contrib/chat/common/chatWidgetHistoryService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; export interface IChatViewTitleActionContext { @@ -227,20 +220,6 @@ export function registerChatActions() { } }); - class ChatAccessibilityHelpContribution extends Disposable { - static ID: 'chatAccessibilityHelpContribution'; - constructor() { - super(); - this._register(AccessibilityHelpAction.addImplementation(105, 'panelChat', async accessor => { - const codeEditor = accessor.get(ICodeEditorService).getActiveCodeEditor() || accessor.get(ICodeEditorService).getFocusedCodeEditor(); - runAccessibilityHelpAction(accessor, codeEditor ?? undefined, 'panelChat'); - }, ContextKeyExpr.or(CONTEXT_IN_CHAT_SESSION, CONTEXT_RESPONSE, CONTEXT_REQUEST))); - } - } - - const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); - workbenchRegistry.registerWorkbenchContribution(ChatAccessibilityHelpContribution, LifecyclePhase.Eventually); - registerAction2(class FocusChatInputAction extends Action2 { constructor() { super({ diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index e6d70783da2..de2ed8deb5f 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -3,11 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IMarkdownString, MarkdownString, isMarkdownString } from 'vs/base/common/htmlContent'; +import { MarkdownString, isMarkdownString } from 'vs/base/common/htmlContent'; import { Disposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { isMacintosh } from 'vs/base/common/platform'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import * as nls from 'vs/nls'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; @@ -18,10 +17,6 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { EditorPaneDescriptor, IEditorPaneRegistry } from 'vs/workbench/browser/editor'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; import { EditorExtensions, IEditorFactoryRegistry } from 'vs/workbench/common/editor'; -import { AccessibilityVerbositySettingId, AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; -import { AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; -import { alertFocusChange } from 'vs/workbench/contrib/accessibility/browser/accessibleViewContributions'; import { registerChatActions } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; import { ACTION_ID_NEW_CHAT, registerNewChatActions } from 'vs/workbench/contrib/chat/browser/actions/chatClearActions'; import { registerChatCodeBlockActions, registerChatCodeCompareBlockActions } from 'vs/workbench/contrib/chat/browser/actions/chatCodeblockActions'; @@ -32,7 +27,7 @@ import { registerChatExportActions } from 'vs/workbench/contrib/chat/browser/act import { registerMoveActions } from 'vs/workbench/contrib/chat/browser/actions/chatMoveActions'; import { registerQuickChatActions } from 'vs/workbench/contrib/chat/browser/actions/chatQuickInputActions'; import { registerChatTitleActions } from 'vs/workbench/contrib/chat/browser/actions/chatTitleActions'; -import { IChatAccessibilityService, IChatCodeBlockContextProviderService, IChatWidget, IChatWidgetService, IQuickChatService } from 'vs/workbench/contrib/chat/browser/chat'; +import { IChatAccessibilityService, IChatCodeBlockContextProviderService, IChatWidgetService, IQuickChatService } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chatAccessibilityService'; import { ChatEditor, IChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatEditor'; import { ChatEditorInput, ChatEditorInputSerializer } from 'vs/workbench/contrib/chat/browser/chatEditorInput'; @@ -43,14 +38,11 @@ import { ChatCodeBlockContextProviderService } from 'vs/workbench/contrib/chat/b import 'vs/workbench/contrib/chat/browser/contrib/chatHistoryVariables'; import 'vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib'; import { ChatAgentLocation, ChatAgentService, IChatAgentService, IChatAgentNameService, ChatAgentNameService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { CONTEXT_IN_CHAT_SESSION } from 'vs/workbench/contrib/chat/common/chatContextKeys'; -import { ChatWelcomeMessageModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; 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 { isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { ChatWidgetHistoryService, IChatWidgetHistoryService } from 'vs/workbench/contrib/chat/common/chatWidgetHistoryService'; import { ILanguageModelsService, LanguageModelsService } from 'vs/workbench/contrib/chat/common/languageModels'; import { ILanguageModelStatsService, LanguageModelStatsService } from 'vs/workbench/contrib/chat/common/languageModelStats'; @@ -59,6 +51,9 @@ import { IEditorResolverService, RegisteredEditorPriority } from 'vs/workbench/s import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import '../common/chatColors'; import { ChatExtensionPointHandler } from 'vs/workbench/contrib/chat/browser/chatParticipantContributions'; +import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { ChatResponseAccessibleView } from 'vs/workbench/contrib/chat/browser/chatResponseAccessibleView'; +import { ChatAccessibilityHelp } from 'vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp'; // Register configuration const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); @@ -143,89 +138,8 @@ class ChatResolverContribution extends Disposable { } } -class ChatAccessibleViewContribution extends Disposable { - static ID: 'chatAccessibleViewContribution'; - constructor() { - super(); - this._register(AccessibleViewAction.addImplementation(100, 'panelChat', accessor => { - const accessibleViewService = accessor.get(IAccessibleViewService); - const widgetService = accessor.get(IChatWidgetService); - const codeEditorService = accessor.get(ICodeEditorService); - return renderAccessibleView(accessibleViewService, widgetService, codeEditorService, true); - function renderAccessibleView(accessibleViewService: IAccessibleViewService, widgetService: IChatWidgetService, codeEditorService: ICodeEditorService, initialRender?: boolean): boolean { - const widget = widgetService.lastFocusedWidget; - if (!widget) { - return false; - } - const chatInputFocused = initialRender && !!codeEditorService.getFocusedCodeEditor(); - if (initialRender && chatInputFocused) { - widget.focusLastMessage(); - } - - if (!widget) { - return false; - } - - const verifiedWidget: IChatWidget = widget; - const focusedItem = verifiedWidget.getFocus(); - - if (!focusedItem) { - return false; - } - - widget.focus(focusedItem); - const isWelcome = focusedItem instanceof ChatWelcomeMessageModel; - let responseContent = isResponseVM(focusedItem) ? focusedItem.response.asString() : undefined; - if (isWelcome) { - const welcomeReplyContents = []; - for (const content of focusedItem.content) { - if (Array.isArray(content)) { - welcomeReplyContents.push(...content.map(m => m.message)); - } else { - welcomeReplyContents.push((content as IMarkdownString).value); - } - } - responseContent = welcomeReplyContents.join('\n'); - } - if (!responseContent && 'errorDetails' in focusedItem && focusedItem.errorDetails) { - responseContent = focusedItem.errorDetails.message; - } - if (!responseContent) { - return false; - } - const responses = verifiedWidget.viewModel?.getItems().filter(i => isResponseVM(i)); - const length = responses?.length; - const responseIndex = responses?.findIndex(i => i === focusedItem); - - accessibleViewService.show({ - id: AccessibleViewProviderId.Chat, - verbositySettingKey: AccessibilityVerbositySettingId.Chat, - provideContent(): string { return responseContent!; }, - onClose() { - verifiedWidget.reveal(focusedItem); - if (chatInputFocused) { - verifiedWidget.focusInput(); - } else { - verifiedWidget.focus(focusedItem); - } - }, - next() { - verifiedWidget.moveFocus(focusedItem, 'next'); - alertFocusChange(responseIndex, length, 'next'); - renderAccessibleView(accessibleViewService, widgetService, codeEditorService); - }, - previous() { - verifiedWidget.moveFocus(focusedItem, 'previous'); - alertFocusChange(responseIndex, length, 'previous'); - renderAccessibleView(accessibleViewService, widgetService, codeEditorService); - }, - options: { type: AccessibleViewType.View } - }); - return true; - } - }, CONTEXT_IN_CHAT_SESSION)); - } -} +AccessibleViewRegistry.register(new ChatResponseAccessibleView()); +AccessibleViewRegistry.register(new ChatAccessibilityHelp()); class ChatSlashStaticSlashCommandsContribution extends Disposable { @@ -318,7 +232,6 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable { const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); registerWorkbenchContribution2(ChatResolverContribution.ID, ChatResolverContribution, WorkbenchPhase.BlockStartup); -workbenchContributionsRegistry.registerWorkbenchContribution(ChatAccessibleViewContribution, LifecyclePhase.Eventually); workbenchContributionsRegistry.registerWorkbenchContribution(ChatSlashStaticSlashCommandsContribution, LifecyclePhase.Eventually); Registry.as(EditorExtensions.EditorFactory).registerEditorSerializer(ChatEditorInput.TypeID, ChatEditorInputSerializer); registerWorkbenchContribution2(ChatExtensionPointHandler.ID, ChatExtensionPointHandler, WorkbenchPhase.BlockStartup); diff --git a/src/vs/workbench/contrib/chat/browser/chatAccessibilityProvider.ts b/src/vs/workbench/contrib/chat/browser/chatAccessibilityProvider.ts index 933a8940869..5eb8edf7f6d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAccessibilityProvider.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAccessibilityProvider.ts @@ -8,7 +8,7 @@ import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { marked } from 'vs/base/common/marked/marked'; import { localize } from 'vs/nls'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; +import { IAccessibleViewService } from 'vs/platform/accessibility/browser/accessibleView'; import { ChatTreeItem } from 'vs/workbench/contrib/chat/browser/chat'; import { isRequestVM, isResponseVM, isWelcomeVM, IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; diff --git a/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts b/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts new file mode 100644 index 00000000000..2babe2b5131 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts @@ -0,0 +1,97 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { AccessibleViewProviderId, AccessibleViewType } from 'vs/platform/accessibility/browser/accessibleView'; +import { alertAccessibleViewFocusChange, IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { IChatWidgetService, IChatWidget } from 'vs/workbench/contrib/chat/browser/chat'; +import { CONTEXT_IN_CHAT_SESSION } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { ChatWelcomeMessageModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; + +export class ChatResponseAccessibleView implements IAccessibleViewImplentation { + readonly priority = 100; + readonly name = 'panelChat'; + readonly type = AccessibleViewType.View; + readonly when = CONTEXT_IN_CHAT_SESSION; + getProvider(accessor: ServicesAccessor) { + const widgetService = accessor.get(IChatWidgetService); + const codeEditorService = accessor.get(ICodeEditorService); + return resolveProvider(widgetService, codeEditorService, true); + function resolveProvider(widgetService: IChatWidgetService, codeEditorService: ICodeEditorService, initialRender?: boolean) { + const widget = widgetService.lastFocusedWidget; + if (!widget) { + return; + } + const chatInputFocused = initialRender && !!codeEditorService.getFocusedCodeEditor(); + if (initialRender && chatInputFocused) { + widget.focusLastMessage(); + } + + if (!widget) { + return; + } + + const verifiedWidget: IChatWidget = widget; + const focusedItem = verifiedWidget.getFocus(); + + if (!focusedItem) { + return; + } + + widget.focus(focusedItem); + const isWelcome = focusedItem instanceof ChatWelcomeMessageModel; + let responseContent = isResponseVM(focusedItem) ? focusedItem.response.asString() : undefined; + if (isWelcome) { + const welcomeReplyContents = []; + for (const content of focusedItem.content) { + if (Array.isArray(content)) { + welcomeReplyContents.push(...content.map(m => m.message)); + } else { + welcomeReplyContents.push((content as IMarkdownString).value); + } + } + responseContent = welcomeReplyContents.join('\n'); + } + if (!responseContent && 'errorDetails' in focusedItem && focusedItem.errorDetails) { + responseContent = focusedItem.errorDetails.message; + } + if (!responseContent) { + return; + } + const responses = verifiedWidget.viewModel?.getItems().filter(i => isResponseVM(i)); + const length = responses?.length; + const responseIndex = responses?.findIndex(i => i === focusedItem); + + return { + id: AccessibleViewProviderId.Chat, + verbositySettingKey: AccessibilityVerbositySettingId.Chat, + provideContent(): string { return responseContent!; }, + onClose() { + verifiedWidget.reveal(focusedItem); + if (chatInputFocused) { + verifiedWidget.focusInput(); + } else { + verifiedWidget.focus(focusedItem); + } + }, + next() { + verifiedWidget.moveFocus(focusedItem, 'next'); + alertAccessibleViewFocusChange(responseIndex, length, 'next'); + resolveProvider(widgetService, codeEditorService); + }, + previous() { + verifiedWidget.moveFocus(focusedItem, 'previous'); + alertAccessibleViewFocusChange(responseIndex, length, 'previous'); + resolveProvider(widgetService, codeEditorService); + }, + options: { type: AccessibleViewType.View } + }; + } + } +} diff --git a/src/vs/workbench/contrib/codeEditor/browser/diffEditorAccessibilityHelp.ts b/src/vs/workbench/contrib/codeEditor/browser/diffEditorAccessibilityHelp.ts new file mode 100644 index 00000000000..dbc7ffb7a78 --- /dev/null +++ b/src/vs/workbench/contrib/codeEditor/browser/diffEditorAccessibilityHelp.ts @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { AccessibleDiffViewerNext, AccessibleDiffViewerPrev } from 'vs/editor/browser/widget/diffEditor/commands'; +import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget'; +import { localize } from 'vs/nls'; +import { AccessibleViewProviderId, AccessibleViewType } from 'vs/platform/accessibility/browser/accessibleView'; +import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { ContextKeyEqualsExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { getCommentCommandInfo } from 'vs/workbench/contrib/accessibility/browser/editorAccessibilityHelp'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; + +export class DiffEditorAccessibilityHelp implements IAccessibleViewImplentation { + readonly priority = 105; + readonly name = 'diff-editor'; + readonly when = ContextKeyEqualsExpr.create('isInDiffEditor', true); + readonly type = AccessibleViewType.Help; + getProvider(accessor: ServicesAccessor) { + const editorService = accessor.get(IEditorService); + const codeEditorService = accessor.get(ICodeEditorService); + const keybindingService = accessor.get(IKeybindingService); + const contextKeyService = accessor.get(IContextKeyService); + + if (!(editorService.activeTextEditorControl instanceof DiffEditorWidget)) { + return; + } + + const codeEditor = codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor(); + if (!codeEditor) { + return; + } + + const next = keybindingService.lookupKeybinding(AccessibleDiffViewerNext.id)?.getAriaLabel(); + const previous = keybindingService.lookupKeybinding(AccessibleDiffViewerPrev.id)?.getAriaLabel(); + let switchSides; + const switchSidesKb = keybindingService.lookupKeybinding('diffEditor.switchSide')?.getAriaLabel(); + if (switchSidesKb) { + switchSides = localize('msg3', "Run the command Diff Editor: Switch Side ({0}) to toggle between the original and modified editors.", switchSidesKb); + } else { + switchSides = localize('switchSidesNoKb', "Run the command Diff Editor: Switch Side, which is currently not triggerable via keybinding, to toggle between the original and modified editors."); + } + + const diffEditorActiveAnnouncement = localize('msg5', "The setting, accessibility.verbosity.diffEditorActive, controls if a diff editor announcement is made when it becomes the active editor."); + + const keys = ['accessibility.signals.diffLineDeleted', 'accessibility.signals.diffLineInserted', 'accessibility.signals.diffLineModified']; + const content = [ + localize('msg1', "You are in a diff editor."), + localize('msg2', "View the next ({0}) or previous ({1}) diff in diff review mode, which is optimized for screen readers.", next, previous), + switchSides, + diffEditorActiveAnnouncement, + localize('msg4', "To control which accessibility signals should be played, the following settings can be configured: {0}.", keys.join(', ')), + ]; + const commentCommandInfo = getCommentCommandInfo(keybindingService, contextKeyService, codeEditor); + if (commentCommandInfo) { + content.push(commentCommandInfo); + } + return { + id: AccessibleViewProviderId.DiffEditor, + verbositySettingKey: AccessibilityVerbositySettingId.DiffEditor, + provideContent: () => content.join('\n\n'), + onClose: () => { + codeEditor.focus(); + }, + options: { type: AccessibleViewType.Help } + }; + } +} diff --git a/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts b/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts index b47c9bc5642..27448b3a815 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts @@ -3,29 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import { autorunWithStore, observableFromEvent } from 'vs/base/common/observable'; import { IDiffEditor } from 'vs/editor/browser/editorBrowser'; import { registerDiffEditorContribution } from 'vs/editor/browser/editorExtensions'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { AccessibleDiffViewerNext, AccessibleDiffViewerPrev } from 'vs/editor/browser/widget/diffEditor/commands'; -import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget'; import { EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/embeddedDiffEditorWidget'; import { IDiffEditorContribution } from 'vs/editor/common/editorCommon'; import { localize } from 'vs/nls'; +import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ContextKeyEqualsExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { Registry } from 'vs/platform/registry/common/platform'; import { FloatingEditorClickWidget } from 'vs/workbench/browser/codeeditor'; import { Extensions, IConfigurationMigrationRegistry } from 'vs/workbench/common/configuration'; -import { AccessibilityVerbositySettingId, AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; -import { AccessibilityHelpAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; -import { getCommentCommandInfo } from 'vs/workbench/contrib/accessibility/browser/editorAccessibilityHelp'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { DiffEditorAccessibilityHelp } from 'vs/workbench/contrib/codeEditor/browser/diffEditorAccessibilityHelp'; class DiffEditorHelperContribution extends Disposable implements IDiffEditorContribution { public static readonly ID = 'editor.contrib.diffEditorHelper'; @@ -38,8 +30,6 @@ class DiffEditorHelperContribution extends Disposable implements IDiffEditorCont ) { super(); - this._register(createScreenReaderHelp()); - const isEmbeddedDiffEditor = this._diffEditor instanceof EmbeddedDiffEditorWidget; if (!isEmbeddedDiffEditor) { @@ -83,59 +73,6 @@ class DiffEditorHelperContribution extends Disposable implements IDiffEditorCont } } -function createScreenReaderHelp(): IDisposable { - return AccessibilityHelpAction.addImplementation(105, 'diff-editor', async (accessor) => { - const accessibleViewService = accessor.get(IAccessibleViewService); - const editorService = accessor.get(IEditorService); - const codeEditorService = accessor.get(ICodeEditorService); - const keybindingService = accessor.get(IKeybindingService); - const contextKeyService = accessor.get(IContextKeyService); - - if (!(editorService.activeTextEditorControl instanceof DiffEditorWidget)) { - return; - } - - const codeEditor = codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor(); - if (!codeEditor) { - return; - } - - const next = keybindingService.lookupKeybinding(AccessibleDiffViewerNext.id)?.getAriaLabel(); - const previous = keybindingService.lookupKeybinding(AccessibleDiffViewerPrev.id)?.getAriaLabel(); - let switchSides; - const switchSidesKb = keybindingService.lookupKeybinding('diffEditor.switchSide')?.getAriaLabel(); - if (switchSidesKb) { - switchSides = localize('msg3', "Run the command Diff Editor: Switch Side ({0}) to toggle between the original and modified editors.", switchSidesKb); - } else { - switchSides = localize('switchSidesNoKb', "Run the command Diff Editor: Switch Side, which is currently not triggerable via keybinding, to toggle between the original and modified editors."); - } - - const diffEditorActiveAnnouncement = localize('msg5', "The setting, accessibility.verbosity.diffEditorActive, controls if a diff editor announcement is made when it becomes the active editor."); - - const keys = ['accessibility.signals.diffLineDeleted', 'accessibility.signals.diffLineInserted', 'accessibility.signals.diffLineModified']; - const content = [ - localize('msg1', "You are in a diff editor."), - localize('msg2', "View the next ({0}) or previous ({1}) diff in diff review mode, which is optimized for screen readers.", next, previous), - switchSides, - diffEditorActiveAnnouncement, - localize('msg4', "To control which accessibility signals should be played, the following settings can be configured: {0}.", keys.join(', ')), - ]; - const commentCommandInfo = getCommentCommandInfo(keybindingService, contextKeyService, codeEditor); - if (commentCommandInfo) { - content.push(commentCommandInfo); - } - accessibleViewService.show({ - id: AccessibleViewProviderId.DiffEditor, - verbositySettingKey: AccessibilityVerbositySettingId.DiffEditor, - provideContent: () => content.join('\n\n'), - onClose: () => { - codeEditor.focus(); - }, - options: { type: AccessibleViewType.Help } - }); - }, ContextKeyEqualsExpr.create('isInDiffEditor', true)); -} - registerDiffEditorContribution(DiffEditorHelperContribution.ID, DiffEditorHelperContribution); Registry.as(Extensions.ConfigurationMigration) @@ -148,3 +85,4 @@ Registry.as(Extensions.ConfigurationMigration) ]; } }]); +AccessibleViewRegistry.register(new DiffEditorAccessibilityHelp()); diff --git a/src/vs/workbench/contrib/comments/browser/comments.contribution.ts b/src/vs/workbench/contrib/comments/browser/comments.contribution.ts index 7c2964e6ad8..4f7bfdd711d 100644 --- a/src/vs/workbench/contrib/comments/browser/comments.contribution.ts +++ b/src/vs/workbench/contrib/comments/browser/comments.contribution.ts @@ -25,7 +25,11 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { revealCommentThread } from 'vs/workbench/contrib/comments/browser/commentsController'; import { MarshalledCommentThreadInternal } from 'vs/workbench/common/comments'; -import { accessibleViewCurrentProviderId, accessibleViewIsShown, AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { accessibleViewCurrentProviderId, accessibleViewIsShown } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { AccessibleViewProviderId } from 'vs/platform/accessibility/browser/accessibleView'; +import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { CommentsAccessibleView } from 'vs/workbench/contrib/comments/browser/commentsAccessibleView'; +import { CommentsAccessibilityHelp } from 'vs/workbench/contrib/comments/browser/commentsAccessibility'; registerAction2(class Collapse extends ViewAction { constructor() { @@ -187,3 +191,6 @@ export class UnresolvedCommentsBadge extends Disposable implements IWorkbenchCon } Registry.as(Extensions.Workbench).registerWorkbenchContribution(UnresolvedCommentsBadge, LifecyclePhase.Eventually); + +AccessibleViewRegistry.register(new CommentsAccessibleView()); +AccessibleViewRegistry.register(new CommentsAccessibilityHelp()); diff --git a/src/vs/workbench/contrib/comments/browser/commentsAccessibility.ts b/src/vs/workbench/contrib/comments/browser/commentsAccessibility.ts index 921d9be01b2..2f200bc0788 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsAccessibility.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsAccessibility.ts @@ -3,20 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from 'vs/base/common/lifecycle'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { AccessibleViewType, IAccessibleViewContentProvider, IAccessibleViewOptions, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; -import { AccessibilityHelpAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ctxCommentEditorFocused } from 'vs/workbench/contrib/comments/browser/simpleCommentEditor'; import { CommentContextKeys } from 'vs/workbench/contrib/comments/common/commentContextKeys'; import * as nls from 'vs/nls'; -import { AccessibilityVerbositySettingId, AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import * as strings from 'vs/base/common/strings'; import { getActiveElement } from 'vs/base/browser/dom'; import { CommentCommandId } from 'vs/workbench/contrib/comments/common/commentCommandIds'; import { ToggleTabFocusModeAction } from 'vs/editor/contrib/toggleTabFocusMode/browser/toggleTabFocusMode'; +import { IAccessibleViewContentProvider, AccessibleViewProviderId, IAccessibleViewOptions, AccessibleViewType } from 'vs/platform/accessibility/browser/accessibleView'; +import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; export namespace CommentAccessibilityHelpNLS { export const intro = nls.localize('intro', "The editor contains commentable range(s). Some useful commands include:"); @@ -72,15 +71,12 @@ export class CommentsAccessibilityHelpProvider implements IAccessibleViewContent } } -export class CommentsAccessibilityHelpContribution extends Disposable { - static ID: 'commentsAccessibilityHelpContribution'; - constructor() { - super(); - this._register(AccessibilityHelpAction.addImplementation(110, 'comments', accessor => { - const instantiationService = accessor.get(IInstantiationService); - const accessibleViewService = accessor.get(IAccessibleViewService); - accessibleViewService.show(instantiationService.createInstance(CommentsAccessibilityHelpProvider)); - return true; - }, ContextKeyExpr.or(ctxCommentEditorFocused, CommentContextKeys.commentFocused))); +export class CommentsAccessibilityHelp implements IAccessibleViewImplentation { + readonly priority = 110; + readonly name = 'comments'; + readonly type = AccessibleViewType.Help; + readonly when = ContextKeyExpr.or(ctxCommentEditorFocused, CommentContextKeys.commentFocused); + getProvider(accessor: ServicesAccessor) { + return accessor.get(IInstantiationService).createInstance(CommentsAccessibilityHelpProvider); } } diff --git a/src/vs/workbench/contrib/comments/browser/commentsAccessibleView.ts b/src/vs/workbench/contrib/comments/browser/commentsAccessibleView.ts new file mode 100644 index 00000000000..2fdac52458d --- /dev/null +++ b/src/vs/workbench/contrib/comments/browser/commentsAccessibleView.ts @@ -0,0 +1,87 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { MarshalledId } from 'vs/base/common/marshallingIds'; +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { AccessibleViewProviderId, AccessibleViewType } from 'vs/platform/accessibility/browser/accessibleView'; +import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { IMenuService } from 'vs/platform/actions/common/actions'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { COMMENTS_VIEW_ID, CommentsMenus } from 'vs/workbench/contrib/comments/browser/commentsTreeViewer'; +import { CommentsPanel, CONTEXT_KEY_HAS_COMMENTS } from 'vs/workbench/contrib/comments/browser/commentsView'; +import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; + +export class CommentsAccessibleView extends Disposable implements IAccessibleViewImplentation { + readonly priority = 90; + readonly name = 'comment'; + readonly when = CONTEXT_KEY_HAS_COMMENTS; + readonly type = AccessibleViewType.View; + getProvider(accessor: ServicesAccessor) { + const contextKeyService = accessor.get(IContextKeyService); + const viewsService = accessor.get(IViewsService); + const menuService = accessor.get(IMenuService); + const commentsView = viewsService.getActiveViewWithId(COMMENTS_VIEW_ID); + if (!commentsView) { + return; + } + const menus = this._register(new CommentsMenus(menuService)); + menus.setContextKeyService(contextKeyService); + + function resolveProvider() { + if (!commentsView) { + return; + } + + const commentNode = commentsView.focusedCommentNode; + const content = commentsView.focusedCommentInfo?.toString(); + if (!commentNode || !content) { + return; + } + const menuActions = [...menus.getResourceContextActions(commentNode)].filter(i => i.enabled); + const actions = menuActions.map(action => { + return { + ...action, + run: () => { + commentsView.focus(); + action.run({ + thread: commentNode.thread, + $mid: MarshalledId.CommentThread, + commentControlHandle: commentNode.controllerHandle, + commentThreadHandle: commentNode.threadHandle, + }); + } + }; + }); + return { + id: AccessibleViewProviderId.Notification, + provideContent: () => { + return content; + }, + onClose(): void { + commentsView.focus(); + }, + next(): void { + commentsView.focus(); + commentsView.focusNextNode(); + resolveProvider(); + }, + previous(): void { + commentsView.focus(); + commentsView.focusPreviousNode(); + resolveProvider(); + }, + verbositySettingKey: AccessibilityVerbositySettingId.Comments, + options: { type: AccessibleViewType.View }, + actions + }; + } + return resolveProvider(); + } + constructor() { + super(); + } +} diff --git a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts index f419804c8d7..896d352efe9 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts @@ -23,10 +23,11 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { CommentContextKeys } from 'vs/workbench/contrib/comments/common/commentContextKeys'; import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from 'vs/platform/accessibility/common/accessibility'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { accessibilityHelpIsShown, accessibleViewCurrentProviderId, AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { accessibilityHelpIsShown, accessibleViewCurrentProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { CommentCommandId } from 'vs/workbench/contrib/comments/common/commentCommandIds'; import { registerWorkbenchContribution2, WorkbenchPhase } from 'vs/workbench/common/contributions'; import { CommentsInputContentProvider } from 'vs/workbench/contrib/comments/browser/commentsInputContentProvider'; +import { AccessibleViewProviderId } from 'vs/platform/accessibility/browser/accessibleView'; registerEditorContribution(ID, CommentController, EditorContributionInstantiation.AfterFirstRender); registerWorkbenchContribution2(CommentsInputContentProvider.ID, CommentsInputContentProvider, WorkbenchPhase.BlockRestore); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts index d2399af6b7b..5effa9d61b6 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts @@ -7,7 +7,7 @@ import { EditorContributionInstantiation, registerEditorContribution } from 'vs/ import { registerAction2 } from 'vs/platform/actions/common/actions'; import { InlineChatController } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; import * as InlineChatActions from 'vs/workbench/contrib/inlineChat/browser/inlineChatActions'; -import { IInlineChatService, INLINE_CHAT_ID, INTERACTIVE_EDITOR_ACCESSIBILITY_HELP_ID } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { IInlineChatService, INLINE_CHAT_ID } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { InlineChatServiceImpl } from 'vs/workbench/contrib/inlineChat/common/inlineChatServiceImpl'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -15,10 +15,11 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle import { InlineChatNotebookContribution } from 'vs/workbench/contrib/inlineChat/browser/inlineChatNotebook'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { InlineChatSavingServiceImpl } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSavingServiceImpl'; -import { InlineChatAccessibleViewContribution } from 'vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView'; +import { InlineChatAccessibleView } from 'vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView'; import { IInlineChatSavingService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSavingService'; import { IInlineChatSessionService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionService'; import { InlineChatSessionServiceImpl } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl'; +import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; // --- browser @@ -28,7 +29,8 @@ registerSingleton(IInlineChatSessionService, InlineChatSessionServiceImpl, Insta registerSingleton(IInlineChatSavingService, InlineChatSavingServiceImpl, InstantiationType.Delayed); registerEditorContribution(INLINE_CHAT_ID, InlineChatController, EditorContributionInstantiation.Eager); // EAGER because of notebook dispose/create of editors -registerEditorContribution(INTERACTIVE_EDITOR_ACCESSIBILITY_HELP_ID, InlineChatActions.InlineAccessibilityHelpContribution, EditorContributionInstantiation.Eventually); + +AccessibleViewRegistry.register(new InlineChatAccessibleView()); registerAction2(InlineChatActions.StartSessionAction); registerAction2(InlineChatActions.CloseAction); @@ -54,4 +56,5 @@ registerAction2(InlineChatActions.CopyRecordings); const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchContributionsRegistry.registerWorkbenchContribution(InlineChatNotebookContribution, LifecyclePhase.Restored); -workbenchContributionsRegistry.registerWorkbenchContribution(InlineChatAccessibleViewContribution, LifecyclePhase.Eventually); + +AccessibleViewRegistry.register(new InlineChatAccessibleView()); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibilityHelp.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibilityHelp.ts new file mode 100644 index 00000000000..d168f54f836 --- /dev/null +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibilityHelp.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { AccessibleViewType } from 'vs/platform/accessibility/browser/accessibleView'; +import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { getChatAccessibilityHelpProvider } from 'vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp'; +import { CTX_INLINE_CHAT_RESPONSE_FOCUSED, CTX_INLINE_CHAT_FOCUSED } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; + +export class InlineChatAccessibilityHelp implements IAccessibleViewImplentation { + readonly priority = 106; + readonly name = 'inlineChat'; + readonly type = AccessibleViewType.Help; + readonly when = ContextKeyExpr.or(CTX_INLINE_CHAT_RESPONSE_FOCUSED, CTX_INLINE_CHAT_FOCUSED); + getProvider(accessor: ServicesAccessor) { + const codeEditor = accessor.get(ICodeEditorService).getActiveCodeEditor() || accessor.get(ICodeEditorService).getFocusedCodeEditor(); + if (!codeEditor) { + return; + } + return getChatAccessibilityHelpProvider(accessor, codeEditor, 'inlineChat'); + } +} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView.ts index fbb0e1b2012..4cac306c23f 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView.ts @@ -5,44 +5,41 @@ import { InlineChatController } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_RESPONSE_FOCUSED } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; -import { AccessibilityVerbositySettingId, AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; -import { Disposable } from 'vs/base/common/lifecycle'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; +import { AccessibleViewProviderId, AccessibleViewType } from 'vs/platform/accessibility/browser/accessibleView'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; -export class InlineChatAccessibleViewContribution extends Disposable { - static ID: 'inlineChatAccessibleViewContribution'; - constructor() { - super(); - this._register(AccessibleViewAction.addImplementation(100, 'inlineChat', accessor => { - const accessibleViewService = accessor.get(IAccessibleViewService); - const codeEditorService = accessor.get(ICodeEditorService); +export class InlineChatAccessibleView implements IAccessibleViewImplentation { + readonly priority = 100; + readonly name = 'inlineChat'; + readonly when = ContextKeyExpr.or(CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_RESPONSE_FOCUSED); + readonly type = AccessibleViewType.View; + getProvider(accessor: ServicesAccessor) { + const codeEditorService = accessor.get(ICodeEditorService); - const editor = (codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor()); - if (!editor) { - return false; - } - const controller = InlineChatController.get(editor); - if (!controller) { - return false; - } - const responseContent = controller?.getMessage(); - if (!responseContent) { - return false; - } - accessibleViewService.show({ - id: AccessibleViewProviderId.InlineChat, - verbositySettingKey: AccessibilityVerbositySettingId.InlineChat, - provideContent(): string { return responseContent; }, - onClose() { - controller.focus(); - }, - - options: { type: AccessibleViewType.View } - }); - return true; - }, ContextKeyExpr.or(CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_RESPONSE_FOCUSED))); + const editor = (codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor()); + if (!editor) { + return; + } + const controller = InlineChatController.get(editor); + if (!controller) { + return; + } + const responseContent = controller?.getMessage(); + if (!responseContent) { + return; + } + return { + id: AccessibleViewProviderId.InlineChat, + verbositySettingKey: AccessibilityVerbositySettingId.InlineChat, + provideContent(): string { return responseContent; }, + onClose() { + controller.focus(); + }, + options: { type: AccessibleViewType.View } + }; } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index 521edfebb17..7ff7d0ab205 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -11,7 +11,7 @@ import { EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/em import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { InlineChatController, InlineChatRunOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; -import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_PROVIDER, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_VISIBLE, MENU_INLINE_CHAT_WIDGET_DISCARD, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_EDIT_MODE, EditMode, CTX_INLINE_CHAT_DOCUMENT_CHANGED, CTX_INLINE_CHAT_DID_EDIT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, ACTION_ACCEPT_CHANGES, CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChatResponseTypes, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_RESPONSE_FOCUSED, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, MENU_INLINE_CHAT_WIDGET, ACTION_TOGGLE_DIFF } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_PROVIDER, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_VISIBLE, MENU_INLINE_CHAT_WIDGET_DISCARD, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_EDIT_MODE, EditMode, CTX_INLINE_CHAT_DOCUMENT_CHANGED, CTX_INLINE_CHAT_DID_EDIT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, ACTION_ACCEPT_CHANGES, CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChatResponseTypes, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, MENU_INLINE_CHAT_WIDGET, ACTION_TOGGLE_DIFF } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { localize, localize2 } from 'vs/nls'; import { Action2, IAction2Options, MenuRegistry } from 'vs/platform/actions/common/actions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; @@ -24,11 +24,8 @@ import { IUntitledTextResourceEditorInput } from 'vs/workbench/common/editor'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { fromNow } from 'vs/base/common/date'; import { IInlineChatSessionService, Recording } from './inlineChatSessionService'; -import { runAccessibilityHelpAction } from 'vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp'; import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from 'vs/platform/accessibility/common/accessibility'; -import { Disposable } from 'vs/base/common/lifecycle'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { AccessibilityHelpAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { ILogService } from 'vs/platform/log/common/log'; @@ -567,15 +564,3 @@ export class ViewInChatAction extends AbstractInlineChatAction { } } -export class InlineAccessibilityHelpContribution extends Disposable { - constructor() { - super(); - this._register(AccessibilityHelpAction.addImplementation(106, 'inlineChat', async accessor => { - const codeEditor = accessor.get(ICodeEditorService).getActiveCodeEditor() || accessor.get(ICodeEditorService).getFocusedCodeEditor(); - if (!codeEditor) { - return; - } - runAccessibilityHelpAction(accessor, codeEditor, 'inlineChat'); - }, ContextKeyExpr.or(CTX_INLINE_CHAT_RESPONSE_FOCUSED, CTX_INLINE_CHAT_FOCUSED))); - } -} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index 5a122b0fe0a..518a5c19f74 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -32,7 +32,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { asCssVariable, asCssVariableName, editorBackground, editorForeground, inputBackground } from 'vs/platform/theme/common/colorRegistry'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; +import { IAccessibleViewService } from 'vs/platform/accessibility/browser/accessibleView'; import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; import { ChatFollowups } from 'vs/workbench/contrib/chat/browser/chatFollowups'; import { ChatModel, IChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; 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 e202d8dfe58..1c658e42c5a 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts @@ -29,7 +29,7 @@ import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKe import { IEditorProgressService, IProgressRunner } from 'vs/platform/progress/common/progress'; 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 { IAccessibleViewService } from 'vs/platform/accessibility/browser/accessibleView'; import { IChatAccessibilityService, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatAgentLocation, ChatAgentService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts index 4e3f576d720..8c2b1e99ff8 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts @@ -24,7 +24,6 @@ import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKe import { IEditorProgressService, IProgressRunner } from 'vs/platform/progress/common/progress'; 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 { IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { IInlineChatSavingService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSavingService'; @@ -57,6 +56,7 @@ import { IChatAgentService, ChatAgentService, ChatAgentLocation } from 'vs/workb import { ChatVariablesService } from 'vs/workbench/contrib/chat/browser/chatVariables'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { TestCommandService } from 'vs/editor/test/browser/editorTestServices'; +import { IAccessibleViewService } from 'vs/platform/accessibility/browser/accessibleView'; suite('InlineChatSession', function () { diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 157c1a848af..4be869da481 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -118,12 +118,10 @@ import { NotebookKernelHistoryService } from 'vs/workbench/contrib/notebook/brow import { INotebookLoggingService } from 'vs/workbench/contrib/notebook/common/notebookLoggingService'; import { NotebookLoggingService } from 'vs/workbench/contrib/notebook/browser/services/notebookLoggingServiceImpl'; import product from 'vs/platform/product/common/product'; -import { NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; -import { runAccessibilityHelpAction, showAccessibleOutput } from 'vs/workbench/contrib/notebook/browser/notebookAccessibility'; -import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { AccessibilityHelpAction, AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; import { NotebookVariables } from 'vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariables'; +import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { NotebookAccessibilityHelp } from 'vs/workbench/contrib/notebook/browser/notebookAccessibilityHelp'; +import { NotebookAccessibleView } from 'vs/workbench/contrib/notebook/browser/notebookAccessibleView'; /*--------------------------------------------------------------------------------------------- */ @@ -702,37 +700,6 @@ class NotebookLanguageSelectorScoreRefine { } } -class NotebookAccessibilityHelpContribution extends Disposable { - static ID: 'notebookAccessibilityHelpContribution'; - constructor() { - super(); - this._register(AccessibilityHelpAction.addImplementation(105, 'notebook', async accessor => { - const activeEditor = accessor.get(ICodeEditorService).getActiveCodeEditor() - || accessor.get(ICodeEditorService).getFocusedCodeEditor() - || accessor.get(IEditorService).activeEditorPane; - - if (activeEditor) { - runAccessibilityHelpAction(accessor, activeEditor); - } - }, NOTEBOOK_IS_ACTIVE_EDITOR)); - } -} - -class NotebookAccessibleViewContribution extends Disposable { - static ID: 'chatAccessibleViewContribution'; - constructor() { - super(); - this._register(AccessibleViewAction.addImplementation(100, 'notebook', accessor => { - const accessibleViewService = accessor.get(IAccessibleViewService); - const editorService = accessor.get(IEditorService); - - return showAccessibleOutput(accessibleViewService, editorService); - }, - ContextKeyExpr.and(NOTEBOOK_OUTPUT_FOCUSED, ContextKeyExpr.equals('resourceExtname', '.ipynb')) - )); - } -} - const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); registerWorkbenchContribution2(NotebookContribution.ID, NotebookContribution, WorkbenchPhase.BlockStartup); registerWorkbenchContribution2(CellContentProvider.ID, CellContentProvider, WorkbenchPhase.BlockStartup); @@ -741,10 +708,11 @@ registerWorkbenchContribution2(RegisterSchemasContribution.ID, RegisterSchemasCo registerWorkbenchContribution2(NotebookEditorManager.ID, NotebookEditorManager, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(NotebookLanguageSelectorScoreRefine.ID, NotebookLanguageSelectorScoreRefine, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(SimpleNotebookWorkingCopyEditorHandler.ID, SimpleNotebookWorkingCopyEditorHandler, WorkbenchPhase.BlockRestore); -workbenchContributionsRegistry.registerWorkbenchContribution(NotebookAccessibilityHelpContribution, LifecyclePhase.Eventually); -workbenchContributionsRegistry.registerWorkbenchContribution(NotebookAccessibleViewContribution, LifecyclePhase.Eventually); workbenchContributionsRegistry.registerWorkbenchContribution(NotebookVariables, LifecyclePhase.Eventually); +AccessibleViewRegistry.register(new NotebookAccessibleView()); +AccessibleViewRegistry.register(new NotebookAccessibilityHelp()); + registerSingleton(INotebookService, NotebookService, InstantiationType.Delayed); registerSingleton(INotebookEditorWorkerService, NotebookEditorWorkerServiceImpl, InstantiationType.Delayed); registerSingleton(INotebookEditorModelResolverService, NotebookModelResolverServiceImpl, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookAccessibility.ts b/src/vs/workbench/contrib/notebook/browser/notebookAccessibility.ts index c6bb3937f91..11a10ed5950 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookAccessibility.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookAccessibility.ts @@ -3,127 +3,3 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from 'vs/nls'; -import { format } from 'vs/base/common/strings'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; -import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; -import { AccessibilityVerbositySettingId, AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { getNotebookEditorFromEditorPane } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { IVisibleEditorPane } from 'vs/workbench/common/editor'; - -export function getAccessibilityHelpText(accessor: ServicesAccessor): string { - const keybindingService = accessor.get(IKeybindingService); - const content = []; - content.push(localize('notebook.overview', 'The notebook view is a collection of code and markdown cells. Code cells can be executed and will produce output directly below the cell.')); - content.push(descriptionForCommand('notebook.cell.edit', - localize('notebook.cell.edit', 'The Edit Cell command ({0}) will focus on the cell input.'), - localize('notebook.cell.editNoKb', 'The Edit Cell command will focus on the cell input and is currently not triggerable by a keybinding.'), keybindingService)); - content.push(descriptionForCommand('notebook.cell.quitEdit', - localize('notebook.cell.quitEdit', 'The Quit Edit command ({0}) will set focus on the cell container. The default (Escape) key may need to be pressed twice first exit the virtual cursor if active.'), - localize('notebook.cell.quitEditNoKb', 'The Quit Edit command will set focus on the cell container and is currently not triggerable by a keybinding.'), keybindingService)); - content.push(descriptionForCommand('notebook.cell.focusInOutput', - localize('notebook.cell.focusInOutput', 'The Focus Output command ({0}) will set focus in the cell\'s output.'), - localize('notebook.cell.focusInOutputNoKb', 'The Quit Edit command will set focus in the cell\'s output and is currently not triggerable by a keybinding.'), keybindingService)); - content.push(descriptionForCommand('notebook.focusNextEditor', - localize('notebook.focusNextEditor', 'The Focus Next Cell Editor command ({0}) will set focus in the next cell\'s editor.'), - localize('notebook.focusNextEditorNoKb', 'The Focus Next Cell Editor command will set focus in the next cell\'s editor and is currently not triggerable by a keybinding.'), keybindingService)); - content.push(descriptionForCommand('notebook.focusPreviousEditor', - localize('notebook.focusPreviousEditor', 'The Focus Previous Cell Editor command ({0}) will set focus in the previous cell\'s editor.'), - localize('notebook.focusPreviousEditorNoKb', 'The Focus Previous Cell Editor command will set focus in the previous cell\'s editor and is currently not triggerable by a keybinding.'), keybindingService)); - content.push(localize('notebook.cellNavigation', 'The up and down arrows will also move focus between cells while focused on the outer cell container.')); - content.push(descriptionForCommand('notebook.cell.executeAndFocusContainer', - localize('notebook.cell.executeAndFocusContainer', 'The Execute Cell command ({0}) executes the cell that currently has focus.',), - localize('notebook.cell.executeAndFocusContainerNoKb', 'The Execute Cell command executes the cell that currently has focus and is currently not triggerable by a keybinding.'), keybindingService)); - content.push(localize('notebook.cell.insertCodeCellBelowAndFocusContainer', 'The Insert Cell Above/Below commands will create new empty code cells')); - content.push(localize('notebook.changeCellType', 'The Change Cell to Code/Markdown commands are used to switch between cell types.')); - - - return content.join('\n\n'); -} - -function descriptionForCommand(commandId: string, msg: string, noKbMsg: string, keybindingService: IKeybindingService): string { - const kb = keybindingService.lookupKeybinding(commandId); - if (kb) { - return format(msg, kb.getAriaLabel()); - } - return format(noKbMsg, commandId); -} - -export async function runAccessibilityHelpAction(accessor: ServicesAccessor, editor: ICodeEditor | IVisibleEditorPane): Promise { - const accessibleViewService = accessor.get(IAccessibleViewService); - const helpText = getAccessibilityHelpText(accessor); - accessibleViewService.show({ - id: AccessibleViewProviderId.Notebook, - verbositySettingKey: AccessibilityVerbositySettingId.Notebook, - provideContent: () => helpText, - onClose: () => { - editor.focus(); - }, - options: { type: AccessibleViewType.Help } - }); -} - -export function showAccessibleOutput(accessibleViewService: IAccessibleViewService, editorService: IEditorService) { - const activePane = editorService.activeEditorPane; - const notebookEditor = getNotebookEditorFromEditorPane(activePane); - const notebookViewModel = notebookEditor?.getViewModel(); - const selections = notebookViewModel?.getSelections(); - const notebookDocument = notebookViewModel?.notebookDocument; - - if (!selections || !notebookDocument || !notebookEditor?.textModel) { - return false; - } - - const viewCell = notebookViewModel.viewCells[selections[0].start]; - let outputContent = ''; - const decoder = new TextDecoder(); - for (let i = 0; i < viewCell.outputsViewModels.length; i++) { - const outputViewModel = viewCell.outputsViewModels[i]; - const outputTextModel = viewCell.model.outputs[i]; - const [mimeTypes, pick] = outputViewModel.resolveMimeTypes(notebookEditor.textModel, undefined); - const mimeType = mimeTypes[pick].mimeType; - let buffer = outputTextModel.outputs.find(output => output.mime === mimeType); - - if (!buffer || mimeType.startsWith('image')) { - buffer = outputTextModel.outputs.find(output => !output.mime.startsWith('image')); - } - - let text = `${mimeType}`; // default in case we can't get the text value for some reason. - if (buffer) { - const charLimit = 100_000; - text = decoder.decode(buffer.data.slice(0, charLimit).buffer); - - if (buffer.data.byteLength > charLimit) { - text = text + '...(truncated)'; - } - - if (mimeType.endsWith('error')) { - text = text.replace(/\\u001b\[[0-9;]*m/gi, '').replaceAll('\\n', '\n'); - } - } - - const index = viewCell.outputsViewModels.length > 1 - ? `Cell output ${i + 1} of ${viewCell.outputsViewModels.length}\n` - : ''; - outputContent = outputContent.concat(`${index}${text}\n`); - } - - if (!outputContent) { - return false; - } - - accessibleViewService.show({ - id: AccessibleViewProviderId.Notebook, - verbositySettingKey: AccessibilityVerbositySettingId.Notebook, - provideContent(): string { return outputContent; }, - onClose() { - notebookEditor?.setFocus(selections[0]); - activePane?.focus(); - }, - options: { type: AccessibleViewType.View } - }); - return true; -} diff --git a/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityHelp.ts b/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityHelp.ts new file mode 100644 index 00000000000..24ceaf061fa --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityHelp.ts @@ -0,0 +1,86 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { localize } from 'vs/nls'; +import { format } from 'vs/base/common/strings'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { AccessibleViewProviderId, AccessibleViewType } from 'vs/platform/accessibility/browser/accessibleView'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IVisibleEditorPane } from 'vs/workbench/common/editor'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; + +export class NotebookAccessibilityHelp implements IAccessibleViewImplentation { + readonly priority = 105; + readonly name = 'notebook'; + readonly when = NOTEBOOK_IS_ACTIVE_EDITOR; + readonly type: AccessibleViewType = AccessibleViewType.Help; + getProvider(accessor: ServicesAccessor) { + const activeEditor = accessor.get(ICodeEditorService).getActiveCodeEditor() + || accessor.get(ICodeEditorService).getFocusedCodeEditor() + || accessor.get(IEditorService).activeEditorPane; + + if (activeEditor) { + return runAccessibilityHelpAction(accessor, activeEditor); + } + return; + } +} + + + +export function getAccessibilityHelpText(accessor: ServicesAccessor): string { + const keybindingService = accessor.get(IKeybindingService); + const content = []; + content.push(localize('notebook.overview', 'The notebook view is a collection of code and markdown cells. Code cells can be executed and will produce output directly below the cell.')); + content.push(descriptionForCommand('notebook.cell.edit', + localize('notebook.cell.edit', 'The Edit Cell command ({0}) will focus on the cell input.'), + localize('notebook.cell.editNoKb', 'The Edit Cell command will focus on the cell input and is currently not triggerable by a keybinding.'), keybindingService)); + content.push(descriptionForCommand('notebook.cell.quitEdit', + localize('notebook.cell.quitEdit', 'The Quit Edit command ({0}) will set focus on the cell container. The default (Escape) key may need to be pressed twice first exit the virtual cursor if active.'), + localize('notebook.cell.quitEditNoKb', 'The Quit Edit command will set focus on the cell container and is currently not triggerable by a keybinding.'), keybindingService)); + content.push(descriptionForCommand('notebook.cell.focusInOutput', + localize('notebook.cell.focusInOutput', 'The Focus Output command ({0}) will set focus in the cell\'s output.'), + localize('notebook.cell.focusInOutputNoKb', 'The Quit Edit command will set focus in the cell\'s output and is currently not triggerable by a keybinding.'), keybindingService)); + content.push(descriptionForCommand('notebook.focusNextEditor', + localize('notebook.focusNextEditor', 'The Focus Next Cell Editor command ({0}) will set focus in the next cell\'s editor.'), + localize('notebook.focusNextEditorNoKb', 'The Focus Next Cell Editor command will set focus in the next cell\'s editor and is currently not triggerable by a keybinding.'), keybindingService)); + content.push(descriptionForCommand('notebook.focusPreviousEditor', + localize('notebook.focusPreviousEditor', 'The Focus Previous Cell Editor command ({0}) will set focus in the previous cell\'s editor.'), + localize('notebook.focusPreviousEditorNoKb', 'The Focus Previous Cell Editor command will set focus in the previous cell\'s editor and is currently not triggerable by a keybinding.'), keybindingService)); + content.push(localize('notebook.cellNavigation', 'The up and down arrows will also move focus between cells while focused on the outer cell container.')); + content.push(descriptionForCommand('notebook.cell.executeAndFocusContainer', + localize('notebook.cell.executeAndFocusContainer', 'The Execute Cell command ({0}) executes the cell that currently has focus.',), + localize('notebook.cell.executeAndFocusContainerNoKb', 'The Execute Cell command executes the cell that currently has focus and is currently not triggerable by a keybinding.'), keybindingService)); + content.push(localize('notebook.cell.insertCodeCellBelowAndFocusContainer', 'The Insert Cell Above/Below commands will create new empty code cells')); + content.push(localize('notebook.changeCellType', 'The Change Cell to Code/Markdown commands are used to switch between cell types.')); + + + return content.join('\n\n'); +} + +function descriptionForCommand(commandId: string, msg: string, noKbMsg: string, keybindingService: IKeybindingService): string { + const kb = keybindingService.lookupKeybinding(commandId); + if (kb) { + return format(msg, kb.getAriaLabel()); + } + return format(noKbMsg, commandId); +} + +export function runAccessibilityHelpAction(accessor: ServicesAccessor, editor: ICodeEditor | IVisibleEditorPane) { + const helpText = getAccessibilityHelpText(accessor); + return { + id: AccessibleViewProviderId.Notebook, + verbositySettingKey: AccessibilityVerbositySettingId.Notebook, + provideContent: () => helpText, + onClose: () => { + editor.focus(); + }, + options: { type: AccessibleViewType.Help } + }; +} diff --git a/src/vs/workbench/contrib/notebook/browser/notebookAccessibleView.ts b/src/vs/workbench/contrib/notebook/browser/notebookAccessibleView.ts new file mode 100644 index 00000000000..3975c17bb9e --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/notebookAccessibleView.ts @@ -0,0 +1,86 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { AccessibleViewProviderId, AccessibleViewType } from 'vs/platform/accessibility/browser/accessibleView'; +import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { getNotebookEditorFromEditorPane } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; + +export class NotebookAccessibleView implements IAccessibleViewImplentation { + readonly priority = 100; + readonly name = 'notebook'; + readonly type = AccessibleViewType.View; + readonly when = ContextKeyExpr.and(NOTEBOOK_OUTPUT_FOCUSED, ContextKeyExpr.equals('resourceExtname', '.ipynb')); + getProvider(accessor: ServicesAccessor) { + const editorService = accessor.get(IEditorService); + return showAccessibleOutput(editorService); + } +} + + +export function showAccessibleOutput(editorService: IEditorService) { + const activePane = editorService.activeEditorPane; + const notebookEditor = getNotebookEditorFromEditorPane(activePane); + const notebookViewModel = notebookEditor?.getViewModel(); + const selections = notebookViewModel?.getSelections(); + const notebookDocument = notebookViewModel?.notebookDocument; + + if (!selections || !notebookDocument || !notebookEditor?.textModel) { + return; + } + + const viewCell = notebookViewModel.viewCells[selections[0].start]; + let outputContent = ''; + const decoder = new TextDecoder(); + for (let i = 0; i < viewCell.outputsViewModels.length; i++) { + const outputViewModel = viewCell.outputsViewModels[i]; + const outputTextModel = viewCell.model.outputs[i]; + const [mimeTypes, pick] = outputViewModel.resolveMimeTypes(notebookEditor.textModel, undefined); + const mimeType = mimeTypes[pick].mimeType; + let buffer = outputTextModel.outputs.find(output => output.mime === mimeType); + + if (!buffer || mimeType.startsWith('image')) { + buffer = outputTextModel.outputs.find(output => !output.mime.startsWith('image')); + } + + let text = `${mimeType}`; // default in case we can't get the text value for some reason. + if (buffer) { + const charLimit = 100_000; + text = decoder.decode(buffer.data.slice(0, charLimit).buffer); + + if (buffer.data.byteLength > charLimit) { + text = text + '...(truncated)'; + } + + if (mimeType.endsWith('error')) { + text = text.replace(/\\u001b\[[0-9;]*m/gi, '').replaceAll('\\n', '\n'); + } + } + + const index = viewCell.outputsViewModels.length > 1 + ? `Cell output ${i + 1} of ${viewCell.outputsViewModels.length}\n` + : ''; + outputContent = outputContent.concat(`${index}${text}\n`); + } + + if (!outputContent) { + return; + } + + return { + id: AccessibleViewProviderId.Notebook, + verbositySettingKey: AccessibilityVerbositySettingId.Notebook, + provideContent(): string { return outputContent; }, + onClose() { + notebookEditor?.setFocus(selections[0]); + activePane?.focus(); + }, + options: { type: AccessibleViewType.View } + }; +} + diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index f4f0233b5cc..11401eecf70 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -60,10 +60,11 @@ import { TerminalCapability } from 'vs/platform/terminal/common/capabilities/cap import { killTerminalIcon, newTerminalIcon } from 'vs/workbench/contrib/terminal/browser/terminalIcons'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { Iterable } from 'vs/base/common/iterator'; -import { AccessibleViewProviderId, accessibleViewCurrentProviderId, accessibleViewIsShown, accessibleViewOnLastLine } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { accessibleViewCurrentProviderId, accessibleViewIsShown, accessibleViewOnLastLine } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { isKeyboardEvent, isMouseEvent, isPointerEvent } from 'vs/base/browser/dom'; import { editorGroupToColumn } from 'vs/workbench/services/editor/common/editorGroupColumn'; import { InstanceContext } from 'vs/workbench/contrib/terminal/browser/terminalContextMenu'; +import { AccessibleViewProviderId } from 'vs/platform/accessibility/browser/accessibleView'; export const switchTerminalActionViewItemSeparator = '\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'; export const switchTerminalShowTabsTitle = localize('showTerminalTabs', "Show Tabs"); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalRunRecentQuickPick.ts b/src/vs/workbench/contrib/terminal/browser/terminalRunRecentQuickPick.ts index c6f6bdb61d4..a17b660a13c 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalRunRecentQuickPick.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalRunRecentQuickPick.ts @@ -26,8 +26,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { showWithPinnedItems } from 'vs/platform/quickinput/browser/quickPickPin'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; -import { AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { AccessibleViewProviderId, IAccessibleViewService } from 'vs/platform/accessibility/browser/accessibleView'; export async function showRunRecentQuickPick( accessor: ServicesAccessor, diff --git a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts index 98a4ef9657f..5233666f7e4 100644 --- a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts @@ -12,8 +12,6 @@ import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/commo import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ITerminalCommand, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; -import { AccessibleViewProviderId, accessibleViewCurrentProviderId, accessibleViewIsShown } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { IAccessibleViewService, NavigationType } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; import { AccessibilityHelpAction, AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; import { ITerminalContribution, ITerminalInstance, ITerminalService, IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal'; import { registerTerminalAction } from 'vs/workbench/contrib/terminal/browser/terminalActions'; @@ -35,6 +33,8 @@ import { AccessibilitySignal, IAccessibilitySignalService } from 'vs/platform/ac import { alert } from 'vs/base/browser/ui/aria/aria'; import { TerminalAccessibilitySettingId } from 'vs/workbench/contrib/terminalContrib/accessibility/common/terminalAccessibilityConfiguration'; import { TerminalAccessibilityCommandId } from 'vs/workbench/contrib/terminalContrib/accessibility/common/terminal.accessibility'; +import { IAccessibleViewService, AccessibleViewProviderId, NavigationType } from 'vs/platform/accessibility/browser/accessibleView'; +import { accessibleViewCurrentProviderId, accessibleViewIsShown } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; // #region Terminal Contributions diff --git a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibilityHelp.ts b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibilityHelp.ts index ad967c57b5a..25efa6cb270 100644 --- a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibilityHelp.ts @@ -12,8 +12,6 @@ import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/commo import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ShellIntegrationStatus, TerminalSettingId, WindowsShellType } from 'vs/platform/terminal/common/terminal'; -import { AccessibilityVerbositySettingId, AccessibleViewProviderId, accessibleViewCurrentProviderId, accessibleViewIsShown } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { AccessibleViewType, IAccessibleViewContentProvider, IAccessibleViewOptions } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; import { ITerminalInstance, IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; @@ -22,6 +20,8 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { TerminalAccessibilitySettingId } from 'vs/workbench/contrib/terminalContrib/accessibility/common/terminalAccessibilityConfiguration'; import { TerminalAccessibilityCommandId } from 'vs/workbench/contrib/terminalContrib/accessibility/common/terminal.accessibility'; import { TerminalLinksCommandId } from 'vs/workbench/contrib/terminalContrib/links/common/terminal.links'; +import { IAccessibleViewContentProvider, AccessibleViewProviderId, IAccessibleViewOptions, AccessibleViewType } from 'vs/platform/accessibility/browser/accessibleView'; +import { accessibleViewIsShown, accessibleViewCurrentProviderId, AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; export const enum ClassName { Active = 'active', diff --git a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleBufferProvider.ts b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleBufferProvider.ts index 05a1f753a81..329b73e2a31 100644 --- a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleBufferProvider.ts +++ b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleBufferProvider.ts @@ -6,12 +6,12 @@ import { Emitter } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IModelService } from 'vs/editor/common/services/model'; +import { IAccessibleViewContentProvider, AccessibleViewProviderId, IAccessibleViewOptions, AccessibleViewType, IAccessibleViewSymbol } from 'vs/platform/accessibility/browser/accessibleView'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { TerminalCapability, ITerminalCommand } from 'vs/platform/terminal/common/capabilities/capabilities'; import { ICurrentPartialCommand } from 'vs/platform/terminal/common/capabilities/commandDetection/terminalCommand'; -import { AccessibilityVerbositySettingId, AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { AccessibleViewType, IAccessibleViewContentProvider, IAccessibleViewOptions, IAccessibleViewSymbol } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { ITerminalInstance, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { BufferContentTracker } from 'vs/workbench/contrib/terminalContrib/accessibility/browser/bufferContentTracker'; import { TerminalAccessibilitySettingId } from 'vs/workbench/contrib/terminalContrib/accessibility/common/terminalAccessibilityConfiguration'; diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.chat.contribution.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.chat.contribution.ts index 14a3330d3f0..a2c974a0baa 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.chat.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.chat.contribution.ts @@ -3,11 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; import { registerTerminalContribution } from 'vs/workbench/contrib/terminal/browser/terminalExtensions'; -import { TerminalInlineChatAccessibleViewContribution } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView'; +import { TerminalInlineChatAccessibleView } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView'; import { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController'; -import { TerminalChatAccessibilityHelpContribution } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp'; // #region Terminal Contributions @@ -17,13 +15,15 @@ registerTerminalContribution(TerminalChatController.ID, TerminalChatController, // #region Contributions -registerWorkbenchContribution2(TerminalInlineChatAccessibleViewContribution.ID, TerminalInlineChatAccessibleViewContribution, WorkbenchPhase.Eventually); -registerWorkbenchContribution2(TerminalChatAccessibilityHelpContribution.ID, TerminalChatAccessibilityHelpContribution, WorkbenchPhase.Eventually); +AccessibleViewRegistry.register(new TerminalInlineChatAccessibleView()); +AccessibleViewRegistry.register(new TerminalChatAccessibilityHelp()); // #endregion // #region Actions import 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions'; +import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { TerminalChatAccessibilityHelp } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp'; // #endregion diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts index 584bdc753d8..f0bdac465ba 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts @@ -3,44 +3,40 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; +import { AccessibleViewProviderId, AccessibleViewType } from 'vs/platform/accessibility/browser/accessibleView'; +import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { AccessibilityVerbositySettingId, AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; -import { AccessibilityHelpAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalChatCommandId, TerminalChatContextKeys } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; import { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController'; -export class TerminalChatAccessibilityHelpContribution extends Disposable { - static ID = 'terminalChatAccessiblityHelp'; - constructor() { - super(); - this._register(AccessibilityHelpAction.addImplementation(110, 'terminalChat', runAccessibilityHelpAction, TerminalChatContextKeys.focused)); +export class TerminalChatAccessibilityHelp implements IAccessibleViewImplentation { + readonly priority = 110; + readonly name = 'terminalChat'; + readonly when = TerminalChatContextKeys.focused; + readonly type = AccessibleViewType.Help; + getProvider(accessor: ServicesAccessor) { + const terminalService = accessor.get(ITerminalService); + + const instance = terminalService.activeInstance; + if (!instance) { + return; + } + + const helpText = getAccessibilityHelpText(accessor); + return { + id: AccessibleViewProviderId.TerminalChat, + verbositySettingKey: AccessibilityVerbositySettingId.TerminalChat, + provideContent: () => helpText, + onClose: () => TerminalChatController.get(instance)?.focus(), + options: { type: AccessibleViewType.Help } + }; } } -export async function runAccessibilityHelpAction(accessor: ServicesAccessor): Promise { - const accessibleViewService = accessor.get(IAccessibleViewService); - const terminalService = accessor.get(ITerminalService); - - const instance = terminalService.activeInstance; - if (!instance) { - return; - } - - const helpText = getAccessibilityHelpText(accessor); - accessibleViewService.show({ - id: AccessibleViewProviderId.TerminalChat, - verbositySettingKey: AccessibilityVerbositySettingId.TerminalChat, - provideContent: () => helpText, - onClose: () => TerminalChatController.get(instance)?.focus(), - options: { type: AccessibleViewType.Help } - }); -} - export function getAccessibilityHelpText(accessor: ServicesAccessor): string { const keybindingService = accessor.get(IKeybindingService); const content = []; diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts index f5bbe82de03..215f1fe6f05 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts @@ -3,36 +3,34 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from 'vs/base/common/lifecycle'; -import { AccessibilityVerbositySettingId, AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; -import { AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; +import { AccessibleViewProviderId, AccessibleViewType } from 'vs/platform/accessibility/browser/accessibleView'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; -import { TerminalChatContextKeys } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; import { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController'; +import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { TerminalChatContextKeys } from 'vs/workbench/contrib/terminal/browser/terminalContribExports'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -export class TerminalInlineChatAccessibleViewContribution extends Disposable { - static ID: 'terminalInlineChatAccessibleViewContribution'; - constructor() { - super(); - this._register(AccessibleViewAction.addImplementation(105, 'terminalInlineChat', accessor => { - const accessibleViewService = accessor.get(IAccessibleViewService); - const terminalService = accessor.get(ITerminalService); - const controller: TerminalChatController | undefined = terminalService.activeInstance?.getContribution(TerminalChatController.ID) ?? undefined; - if (!controller?.lastResponseContent) { - return false; - } - const responseContent = controller.lastResponseContent; - accessibleViewService.show({ - id: AccessibleViewProviderId.TerminalChat, - verbositySettingKey: AccessibilityVerbositySettingId.InlineChat, - provideContent(): string { return responseContent; }, - onClose() { - controller.focus(); - }, - options: { type: AccessibleViewType.View } - }); - return true; - }, TerminalChatContextKeys.focused)); +export class TerminalInlineChatAccessibleView implements IAccessibleViewImplentation { + readonly priority = 105; + readonly name = 'terminalInlineChat'; + readonly type = AccessibleViewType.View; + readonly when = TerminalChatContextKeys.focused; + getProvider(accessor: ServicesAccessor) { + const terminalService = accessor.get(ITerminalService); + const controller: TerminalChatController | undefined = terminalService.activeInstance?.getContribution(TerminalChatController.ID) ?? undefined; + if (!controller?.lastResponseContent) { + return; + } + const responseContent = controller.lastResponseContent; + return { + id: AccessibleViewProviderId.TerminalChat, + verbositySettingKey: AccessibilityVerbositySettingId.InlineChat, + provideContent(): string { return responseContent; }, + onClose() { + controller.focus(); + }, + options: { type: AccessibleViewType.View } + }; } } diff --git a/src/vs/workbench/contrib/terminalContrib/links/browser/terminal.links.contribution.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/terminal.links.contribution.ts index 89d73f49f25..c6064901930 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/browser/terminal.links.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/browser/terminal.links.contribution.ts @@ -11,7 +11,7 @@ import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { AccessibleViewProviderId, accessibleViewCurrentProviderId, accessibleViewIsShown } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { accessibleViewCurrentProviderId, accessibleViewIsShown } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { IDetachedTerminalInstance, ITerminalContribution, ITerminalInstance, IXtermTerminal, isDetachedTerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; import { registerActiveInstanceAction } from 'vs/workbench/contrib/terminal/browser/terminalActions'; import { registerTerminalContribution } from 'vs/workbench/contrib/terminal/browser/terminalExtensions'; @@ -26,6 +26,7 @@ import { TerminalLinkQuickpick } from 'vs/workbench/contrib/terminalContrib/link import { TerminalLinkResolver } from 'vs/workbench/contrib/terminalContrib/links/browser/terminalLinkResolver'; import type { Terminal as RawXtermTerminal } from '@xterm/xterm'; import { TerminalLinksCommandId } from 'vs/workbench/contrib/terminalContrib/links/common/terminal.links'; +import { AccessibleViewProviderId } from 'vs/platform/accessibility/browser/accessibleView'; // #region Services diff --git a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkQuickpick.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkQuickpick.ts index 80a6e670c4a..0eee6b948a9 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkQuickpick.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkQuickpick.ts @@ -11,8 +11,6 @@ import { IDetectedLinks } from 'vs/workbench/contrib/terminalContrib/links/brows import { TerminalLinkQuickPickEvent, type IDetachedTerminalInstance, type ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; import type { ILink } from '@xterm/xterm'; import { DisposableStore } from 'vs/base/common/lifecycle'; -import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; -import { AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import type { TerminalLink } from 'vs/workbench/contrib/terminalContrib/links/browser/terminalLink'; import { Sequencer, timeout } from 'vs/base/common/async'; import { PickerEditorState } from 'vs/workbench/browser/quickaccess'; @@ -21,6 +19,7 @@ import { TerminalBuiltinLinkType } from 'vs/workbench/contrib/terminalContrib/li import { ILabelService } from 'vs/platform/label/common/label'; import { basenameOrAuthority, dirname } from 'vs/base/common/resources'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { AccessibleViewProviderId, IAccessibleViewService } from 'vs/platform/accessibility/browser/accessibleView'; export class TerminalLinkQuickpick extends DisposableStore { From f8d3bd3909c97a48e61211a9a1c3b3bff3b9195e Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Tue, 7 May 2024 10:31:23 +0200 Subject: [PATCH 013/357] remove old chat message types (#212148) --- .../workbench/api/common/extHost.api.impl.ts | 3 +- .../api/common/extHostLanguageModels.ts | 20 +++-- .../api/common/extHostTypeConverters.ts | 32 +------ src/vs/workbench/api/common/extHostTypes.ts | 13 ++- .../vscode.proposed.chatProvider.d.ts | 4 +- .../vscode.proposed.languageModels.d.ts | 90 +++---------------- 6 files changed, 41 insertions(+), 121 deletions(-) diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 4070489c54e..498573b7f1c 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1733,7 +1733,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ChatResponseTurn: extHostTypes.ChatResponseTurn, ChatLocation: extHostTypes.ChatLocation, LanguageModelChatMessageRole: extHostTypes.LanguageModelChatMessageRole, - LanguageModelChatMessage2: extHostTypes.LanguageModelChatMessage2, + LanguageModelChatMessage: extHostTypes.LanguageModelChatMessage, + LanguageModelChatMessage2: extHostTypes.LanguageModelChatMessage, // TODO@jrieken REMOVE LanguageModelChatSystemMessage: extHostTypes.LanguageModelChatSystemMessage,// TODO@jrieken REMOVE LanguageModelChatUserMessage: extHostTypes.LanguageModelChatUserMessage,// TODO@jrieken REMOVE LanguageModelChatAssistantMessage: extHostTypes.LanguageModelChatAssistantMessage,// TODO@jrieken REMOVE diff --git a/src/vs/workbench/api/common/extHostLanguageModels.ts b/src/vs/workbench/api/common/extHostLanguageModels.ts index a7823af0e4c..eb3cd6373fb 100644 --- a/src/vs/workbench/api/common/extHostLanguageModels.ts +++ b/src/vs/workbench/api/common/extHostLanguageModels.ts @@ -186,11 +186,13 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { this._proxy.$handleProgressChunk(requestId, { index: fragment.index, part: fragment.part }); }); - if (data.provider.provideLanguageModelResponse) { - return data.provider.provideLanguageModelResponse(messages.map(typeConvert.LanguageModelChatMessage.to), options, ExtensionIdentifier.toKey(from), progress, token); - } else { - return data.provider.provideLanguageModelResponse2(messages.map(typeConvert.LanguageModelMessage.to), options, ExtensionIdentifier.toKey(from), progress, token); - } + return data.provider.provideLanguageModelResponse( + messages.map(typeConvert.LanguageModelChatMessage.to), + options, + ExtensionIdentifier.toKey(from), + progress, + token + ); } @@ -324,10 +326,10 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { return res.apiObject; } - private _convertMessages(extension: IExtensionDescription, messages: (vscode.LanguageModelChatMessage2 | vscode.LanguageModelChatMessage)[]) { + private _convertMessages(extension: IExtensionDescription, messages: vscode.LanguageModelChatMessage[]) { const internalMessages: IChatMessage[] = []; for (const message of messages) { - if (message instanceof extHostTypes.LanguageModelChatMessage2) { + if (message instanceof extHostTypes.LanguageModelChatMessage) { if (message.role as number === extHostTypes.LanguageModelChatMessageRole.System) { checkProposedApiEnabled(extension, 'languageModelSystem'); } @@ -336,7 +338,7 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { if (message instanceof extHostTypes.LanguageModelChatSystemMessage) { checkProposedApiEnabled(extension, 'languageModelSystem'); } - internalMessages.push(typeConvert.LanguageModelMessage.from(message)); + internalMessages.push(typeConvert.LanguageModelChatMessage.from(message)); } } return internalMessages; @@ -410,7 +412,7 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { return local.provider.provideTokenCount(value, token); } - return this._proxy.$countTokens(data.identifier, (typeof value === 'string' ? value : typeConvert.LanguageModelMessage.from(value)), token); + return this._proxy.$countTokens(data.identifier, (typeof value === 'string' ? value : typeConvert.LanguageModelChatMessage.from(value)), token); } getLanguageModelInfo(languageModelId: string): vscode.LanguageModelInformation | undefined { diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index f9571a57858..6d3beb0e99a 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -2244,9 +2244,9 @@ export namespace LanguageModelChatMessage { export function to(message: chatProvider.IChatMessage): vscode.LanguageModelChatMessage2 { switch (message.role) { - case chatProvider.ChatMessageRole.System: return new types.LanguageModelChatMessage2(types.LanguageModelChatMessageRole.System, message.content); - case chatProvider.ChatMessageRole.User: return new types.LanguageModelChatMessage2(types.LanguageModelChatMessageRole.User, message.content); - case chatProvider.ChatMessageRole.Assistant: return new types.LanguageModelChatMessage2(types.LanguageModelChatMessageRole.Assistant, message.content); + case chatProvider.ChatMessageRole.System: return new types.LanguageModelChatMessage(types.LanguageModelChatMessageRole.System, message.content); + case chatProvider.ChatMessageRole.User: return new types.LanguageModelChatMessage(types.LanguageModelChatMessageRole.User, message.content); + case chatProvider.ChatMessageRole.Assistant: return new types.LanguageModelChatMessage(types.LanguageModelChatMessageRole.Assistant, message.content); } } @@ -2259,32 +2259,6 @@ export namespace LanguageModelChatMessage { } } -/** - * @deprecated - */ -export namespace LanguageModelMessage { - - export function to(message: chatProvider.IChatMessage): vscode.LanguageModelChatMessage { - switch (message.role) { - case chatProvider.ChatMessageRole.System: return new types.LanguageModelChatSystemMessage(message.content); - case chatProvider.ChatMessageRole.User: return new types.LanguageModelChatUserMessage(message.content); - case chatProvider.ChatMessageRole.Assistant: return new types.LanguageModelChatAssistantMessage(message.content); - } - } - - export function from(message: vscode.LanguageModelChatMessage): chatProvider.IChatMessage { - if (message instanceof types.LanguageModelChatSystemMessage) { - return { role: chatProvider.ChatMessageRole.System, content: message.content }; - } else if (message instanceof types.LanguageModelChatUserMessage) { - return { role: chatProvider.ChatMessageRole.User, content: message.content }; - } else if (message instanceof types.LanguageModelChatAssistantMessage) { - return { role: chatProvider.ChatMessageRole.Assistant, content: message.content }; - } else { - throw new Error('Invalid LanguageModelMessage'); - } - } -} - export namespace ChatResponseMarkdownPart { export function from(part: vscode.ChatResponseMarkdownPart): Dto { return { diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index c52e1c26cc2..c8ba6234f01 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -4430,7 +4430,7 @@ export enum LanguageModelChatMessageRole { System = 3 } -export class LanguageModelChatMessage2 implements vscode.LanguageModelChatMessage2 { +export class LanguageModelChatMessage implements vscode.LanguageModelChatMessage { role: vscode.LanguageModelChatMessageRole; content: string; @@ -4443,14 +4443,20 @@ export class LanguageModelChatMessage2 implements vscode.LanguageModelChatMessag } } +/** + * @deprecated + */ export class LanguageModelChatSystemMessage { content: string; - constructor(content: string) { this.content = content; } } + +/** + * @deprecated + */ export class LanguageModelChatUserMessage { content: string; name: string | undefined; @@ -4461,6 +4467,9 @@ export class LanguageModelChatUserMessage { } } +/** + * @deprecated + */ export class LanguageModelChatAssistantMessage { content: string; name?: string; diff --git a/src/vscode-dts/vscode.proposed.chatProvider.d.ts b/src/vscode-dts/vscode.proposed.chatProvider.d.ts index 6651c4ff859..f0eb2380bcf 100644 --- a/src/vscode-dts/vscode.proposed.chatProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatProvider.d.ts @@ -19,9 +19,7 @@ declare module 'vscode' { onDidReceiveLanguageModelResponse2?: Event<{ readonly extensionId: string; readonly participant?: string; readonly tokenCount?: number }>; - provideLanguageModelResponse?(messages: LanguageModelChatMessage2[], options: { [name: string]: any }, extensionId: string, progress: Progress, token: CancellationToken): Thenable; - - provideLanguageModelResponse2(messages: LanguageModelChatMessage[], options: { [name: string]: any }, extensionId: string, progress: Progress, token: CancellationToken): Thenable; + provideLanguageModelResponse(messages: LanguageModelChatMessage2[], options: { [name: string]: any }, extensionId: string, progress: Progress, token: CancellationToken): Thenable; provideTokenCount(text: string | LanguageModelChatMessage, token: CancellationToken): Thenable; } diff --git a/src/vscode-dts/vscode.proposed.languageModels.d.ts b/src/vscode-dts/vscode.proposed.languageModels.d.ts index be546fe75b1..bc2ba3a6ed9 100644 --- a/src/vscode-dts/vscode.proposed.languageModels.d.ts +++ b/src/vscode-dts/vscode.proposed.languageModels.d.ts @@ -66,8 +66,16 @@ declare module 'vscode' { Assistant = 2 } - // TODO@API name: LanguageModelChatMessage once the deprecated stuff is removed - export class LanguageModelChatMessage2 { + /** + * @deprecated + */ + // TODO@API remove + export type LanguageModelChatMessage2 = LanguageModelChatMessage; + + /** + * Represents a message in a chat. Can assume different roles, like user or assistant. + */ + export class LanguageModelChatMessage { /** * The role of this message. */ @@ -86,85 +94,13 @@ declare module 'vscode' { /** * Create a new user message. * + * @param role The role of the message. * @param content The content of the message. * @param name The optional name of a user for the message. */ constructor(role: LanguageModelChatMessageRole, content: string, name?: string); } - - /** - * @deprecated - */ - export class LanguageModelChatSystemMessage { - - /** - * The content of this message. - */ - content: string; - - /** - * Create a new system message. - * - * @param content The content of the message. - */ - constructor(content: string); - } - - /** - * @deprecated - */ - export class LanguageModelChatUserMessage { - - /** - * The content of this message. - */ - content: string; - - /** - * The optional name of a user for this message. - */ - name: string | undefined; - - /** - * Create a new user message. - * - * @param content The content of the message. - * @param name The optional name of a user for the message. - */ - constructor(content: string, name?: string); - } - - /** - * @deprecated - */ - export class LanguageModelChatAssistantMessage { - - /** - * The content of this message. - */ - content: string; - - /** - * The optional name of a user for this message. - */ - name: string | undefined; - - /** - * Create a new assistant message. - * - * @param content The content of the message. - * @param name The optional name of a user for the message. - */ - constructor(content: string, name?: string); - } - - /** - * Different types of language model messages. - * @deprecated - */ - export type LanguageModelChatMessage = LanguageModelChatSystemMessage | LanguageModelChatUserMessage | LanguageModelChatAssistantMessage; - /** * Represents information about a registered language model. */ @@ -315,7 +251,7 @@ declare module 'vscode' { * @param token A cancellation token which controls the request. See {@link CancellationTokenSource} for how to create one. * @returns A thenable that resolves to a {@link LanguageModelChatResponse}. The promise will reject when the request couldn't be made. */ - export function sendChatRequest(languageModel: string, messages: (LanguageModelChatMessage | LanguageModelChatMessage2)[], options?: LanguageModelChatRequestOptions, token?: CancellationToken): Thenable; + export function sendChatRequest(languageModel: string, messages: LanguageModelChatMessage[], options?: LanguageModelChatRequestOptions, token?: CancellationToken): Thenable; /** * Uses the language model specific tokenzier and computes the length in token of a given message. @@ -330,7 +266,7 @@ declare module 'vscode' { // TODO@API `undefined` when the language model does not support computing token length // ollama has nothing // anthropic suggests to count after the fact https://github.com/anthropics/anthropic-tokenizer-typescript?tab=readme-ov-file#anthropic-typescript-tokenizer - export function computeTokenLength(languageModel: string, text: string | LanguageModelChatMessage | LanguageModelChatMessage2, token?: CancellationToken): Thenable; + export function computeTokenLength(languageModel: string, text: string | LanguageModelChatMessage, token?: CancellationToken): Thenable; } /** From a812bde8e525f585e239b0e44fdae042d71f8d21 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Tue, 7 May 2024 10:49:52 +0200 Subject: [PATCH 014/357] Fix rapid focus flickering (#212153) Fixes #211831 --- src/vs/editor/common/languages.ts | 9 ++- .../api/browser/mainThreadComments.ts | 18 +++-- .../workbench/api/common/extHost.protocol.ts | 4 +- .../workbench/api/common/extHostComments.ts | 14 ++-- .../contrib/comments/browser/commentReply.ts | 10 ++- .../comments/browser/commentService.ts | 8 +- .../comments/browser/commentThreadWidget.ts | 9 ++- .../browser/commentThreadZoneWidget.ts | 10 +-- .../comments/browser/commentsController.ts | 78 ++++++++++--------- .../browser/view/cellParts/cellComments.ts | 2 +- 10 files changed, 90 insertions(+), 72 deletions(-) diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index 7977eca56b4..aabb67dd4aa 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -1877,6 +1877,13 @@ export interface CommentThread { isTemplate: boolean; } +/** + * @internal + */ +export interface AddedCommentThread extends CommentThread { + editorId?: string; +} + /** * @internal */ @@ -1971,7 +1978,7 @@ export interface CommentThreadChangedEvent { /** * Added comment threads. */ - readonly added: CommentThread[]; + readonly added: AddedCommentThread[]; /** * Removed comment threads. diff --git a/src/vs/workbench/api/browser/mainThreadComments.ts b/src/vs/workbench/api/browser/mainThreadComments.ts index 9d97e583ba1..d5e11a86dc1 100644 --- a/src/vs/workbench/api/browser/mainThreadComments.ts +++ b/src/vs/workbench/api/browser/mainThreadComments.ts @@ -180,7 +180,8 @@ export class MainThreadCommentThread implements languages.CommentThread { public resource: string, private _range: T | undefined, private _canReply: boolean, - private _isTemplate: boolean + private _isTemplate: boolean, + public editorId?: string ) { this._isDisposed = false; if (_isTemplate) { @@ -291,7 +292,8 @@ export class MainThreadCommentController implements ICommentController { threadId: string, resource: UriComponents, range: IRange | ICellRange | undefined, - isTemplate: boolean + isTemplate: boolean, + editorId?: string ): languages.CommentThread { const thread = new MainThreadCommentThread( commentThreadHandle, @@ -301,7 +303,8 @@ export class MainThreadCommentController implements ICommentController { URI.revive(resource).toString(), range, true, - isTemplate + isTemplate, + editorId ); this._threads.set(commentThreadHandle, thread); @@ -479,8 +482,8 @@ export class MainThreadCommentController implements ICommentController { return ret; } - createCommentThreadTemplate(resource: UriComponents, range: IRange | undefined): Promise { - return this._proxy.$createCommentThreadTemplate(this.handle, resource, range); + createCommentThreadTemplate(resource: UriComponents, range: IRange | undefined, editorId?: string): Promise { + return this._proxy.$createCommentThreadTemplate(this.handle, resource, range, editorId); } async updateCommentThreadTemplate(threadHandle: number, range: IRange) { @@ -580,7 +583,8 @@ export class MainThreadComments extends Disposable implements MainThreadComments resource: UriComponents, range: IRange | ICellRange | undefined, extensionId: ExtensionIdentifier, - isTemplate: boolean + isTemplate: boolean, + editorId?: string ): languages.CommentThread | undefined { const provider = this._commentControllers.get(handle); @@ -588,7 +592,7 @@ export class MainThreadComments extends Disposable implements MainThreadComments return undefined; } - return provider.createCommentThread(extensionId.value, commentThreadHandle, threadId, resource, range, isTemplate); + return provider.createCommentThread(extensionId.value, commentThreadHandle, threadId, resource, range, isTemplate, editorId); } $updateCommentThread(handle: number, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 9f2dd18fb7f..a2c75722122 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -143,7 +143,7 @@ export interface MainThreadCommentsShape extends IDisposable { $registerCommentController(handle: number, id: string, label: string, extensionId: string): void; $unregisterCommentController(handle: number): void; $updateCommentControllerFeatures(handle: number, features: CommentProviderFeatures): void; - $createCommentThread(handle: number, commentThreadHandle: number, threadId: string, resource: UriComponents, range: IRange | ICellRange | undefined, extensionId: ExtensionIdentifier, isTemplate: boolean): languages.CommentThread | undefined; + $createCommentThread(handle: number, commentThreadHandle: number, threadId: string, resource: UriComponents, range: IRange | ICellRange | undefined, extensionId: ExtensionIdentifier, isTemplate: boolean, editorId?: string): languages.CommentThread | undefined; $updateCommentThread(handle: number, commentThreadHandle: number, threadId: string, resource: UriComponents, changes: CommentThreadChanges): void; $deleteCommentThread(handle: number, commentThreadHandle: number): void; $updateCommentingRanges(handle: number, resourceHints?: languages.CommentingRangeResourceHint): void; @@ -2458,7 +2458,7 @@ export interface ExtHostProgressShape { } export interface ExtHostCommentsShape { - $createCommentThreadTemplate(commentControllerHandle: number, uriComponents: UriComponents, range: IRange | undefined): Promise; + $createCommentThreadTemplate(commentControllerHandle: number, uriComponents: UriComponents, range: IRange | undefined, editorId?: string): Promise; $updateCommentThreadTemplate(commentControllerHandle: number, threadHandle: number, range: IRange): Promise; $deleteCommentThread(commentControllerHandle: number, commentThreadHandle: number): void; $provideCommentingRanges(commentControllerHandle: number, uriComponents: UriComponents, token: CancellationToken): Promise<{ ranges: IRange[]; fileComments: boolean } | undefined>; diff --git a/src/vs/workbench/api/common/extHostComments.ts b/src/vs/workbench/api/common/extHostComments.ts index 38678540e4a..b3f54666152 100644 --- a/src/vs/workbench/api/common/extHostComments.ts +++ b/src/vs/workbench/api/common/extHostComments.ts @@ -160,14 +160,14 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo return commentController.value; } - async $createCommentThreadTemplate(commentControllerHandle: number, uriComponents: UriComponents, range: IRange | undefined): Promise { + async $createCommentThreadTemplate(commentControllerHandle: number, uriComponents: UriComponents, range: IRange | undefined, editorId?: string): Promise { const commentController = this._commentControllers.get(commentControllerHandle); if (!commentController) { return; } - commentController.$createCommentThreadTemplate(uriComponents, range); + commentController.$createCommentThreadTemplate(uriComponents, range, editorId); } async $setActiveComment(controllerHandle: number, commentInfo: { commentThreadHandle: number; uniqueIdInThread?: number }): Promise { @@ -409,7 +409,8 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo private _range: vscode.Range | undefined, private _comments: vscode.Comment[], public readonly extensionDescription: IExtensionDescription, - private _isTemplate: boolean + private _isTemplate: boolean, + editorId?: string ) { this._acceptInputDisposables.value = new DisposableStore(); @@ -424,7 +425,8 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo this._uri, extHostTypeConverter.Range.from(this._range), extensionDescription.identifier, - this._isTemplate + this._isTemplate, + editorId ); this._localDisposables = []; @@ -680,8 +682,8 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo } } - $createCommentThreadTemplate(uriComponents: UriComponents, range: IRange | undefined): ExtHostCommentThread { - const commentThread = new ExtHostCommentThread(this.id, this.handle, undefined, URI.revive(uriComponents), extHostTypeConverter.Range.to(range), [], this._extension, true); + $createCommentThreadTemplate(uriComponents: UriComponents, range: IRange | undefined, editorId?: string): ExtHostCommentThread { + const commentThread = new ExtHostCommentThread(this.id, this.handle, undefined, URI.revive(uriComponents), extHostTypeConverter.Range.to(range), [], this._extension, true, editorId); commentThread.collapsibleState = languages.CommentThreadCollapsibleState.Expanded; this._threads.set(commentThread.handle, commentThread); return commentThread; diff --git a/src/vs/workbench/contrib/comments/browser/commentReply.ts b/src/vs/workbench/contrib/comments/browser/commentReply.ts index 632eeed2e83..209b4a77ba5 100644 --- a/src/vs/workbench/contrib/comments/browser/commentReply.ts +++ b/src/vs/workbench/contrib/comments/browser/commentReply.ts @@ -60,6 +60,7 @@ export class CommentReply extends Disposable { private _commentOptions: languages.CommentOptions | undefined, private _pendingComment: string | undefined, private _parentThread: ICommentThreadWidget, + focus: boolean, private _actionRunDelegate: (() => void) | null, @ICommentService private commentService: ICommentService, @IThemeService private themeService: IThemeService, @@ -75,10 +76,10 @@ export class CommentReply extends Disposable { this.commentEditorIsEmpty = CommentContextKeys.commentIsEmpty.bindTo(this._contextKeyService); this.commentEditorIsEmpty.set(!this._pendingComment); - this.initialize(); + this.initialize(focus); } - async initialize() { + async initialize(focus: boolean) { const hasExistingComments = this._commentThread.comments && this._commentThread.comments.length > 0; const modeId = generateUuid() + '-' + (hasExistingComments ? this._commentThread.threadId : ++INMEM_MODEL_ID); const params = JSON.stringify({ @@ -118,7 +119,7 @@ export class CommentReply extends Disposable { // Only add the additional step of clicking a reply button to expand the textarea when there are existing comments if (hasExistingComments) { this.createReplyButton(this.commentEditor, this.form); - } else if ((this._commentThread.comments && this._commentThread.comments.length === 0) || this._pendingComment) { + } else if (focus && ((this._commentThread.comments && this._commentThread.comments.length === 0) || this._pendingComment)) { this.expandReplyArea(); } this._error = dom.append(this.form, dom.$('.validation-error.hidden')); @@ -140,12 +141,13 @@ export class CommentReply extends Disposable { public updateCommentThread(commentThread: languages.CommentThread) { const isReplying = this.commentEditor.hasTextFocus(); + const oldAndNewBothEmpty = !this._commentThread.comments?.length && !commentThread.comments?.length; if (!this._reviewThreadReplyButton) { this.createReplyButton(this.commentEditor, this.form); } - if (this._commentThread.comments && this._commentThread.comments.length === 0) { + if (this._commentThread.comments && this._commentThread.comments.length === 0 && !oldAndNewBothEmpty) { this.expandReplyArea(); } diff --git a/src/vs/workbench/contrib/comments/browser/commentService.ts b/src/vs/workbench/contrib/comments/browser/commentService.ts index 5771a5d8a59..1d20812ebe5 100644 --- a/src/vs/workbench/contrib/comments/browser/commentService.ts +++ b/src/vs/workbench/contrib/comments/browser/commentService.ts @@ -63,7 +63,7 @@ export interface ICommentController { options?: CommentOptions; contextValue?: string; owner: string; - createCommentThreadTemplate(resource: UriComponents, range: IRange | undefined): Promise; + createCommentThreadTemplate(resource: UriComponents, range: IRange | undefined, editorId?: string): Promise; updateCommentThreadTemplate(threadHandle: number, range: IRange): Promise; deleteCommentThreadMain(commentThreadId: string): void; toggleReaction(uri: URI, thread: CommentThread, comment: Comment, reaction: CommentReaction, token: CancellationToken): Promise; @@ -97,7 +97,7 @@ export interface ICommentService { registerCommentController(uniqueOwner: string, commentControl: ICommentController): void; unregisterCommentController(uniqueOwner?: string): void; getCommentController(uniqueOwner: string): ICommentController | undefined; - createCommentThreadTemplate(uniqueOwner: string, resource: URI, range: Range | undefined): Promise; + createCommentThreadTemplate(uniqueOwner: string, resource: URI, range: Range | undefined, editorId?: string): Promise; updateCommentThreadTemplate(uniqueOwner: string, threadHandle: number, range: Range): Promise; getCommentMenus(uniqueOwner: string): CommentMenus; updateComments(ownerId: string, event: CommentThreadChangedEvent): void; @@ -361,14 +361,14 @@ export class CommentService extends Disposable implements ICommentService { return this._commentControls.get(uniqueOwner); } - async createCommentThreadTemplate(uniqueOwner: string, resource: URI, range: Range | undefined): Promise { + async createCommentThreadTemplate(uniqueOwner: string, resource: URI, range: Range | undefined, editorId?: string): Promise { const commentController = this._commentControls.get(uniqueOwner); if (!commentController) { return; } - return commentController.createCommentThreadTemplate(resource, range); + return commentController.createCommentThreadTemplate(resource, range, editorId); } async updateCommentThreadTemplate(uniqueOwner: string, threadHandle: number, range: Range) { diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts index 43b9b5d3ff4..3c61b8bc26f 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts @@ -230,7 +230,7 @@ export class CommentThreadWidget extends } } - async display(lineHeight: number) { + async display(lineHeight: number, focus: boolean) { const headHeight = Math.max(23, Math.ceil(lineHeight * 1.2)); // 23 is the value of `Math.ceil(lineHeight * 1.2)` with the default editor font size this._header.updateHeight(headHeight); @@ -238,7 +238,7 @@ export class CommentThreadWidget extends // create comment thread only when it supports reply if (this._commentThread.canReply) { - this._createCommentForm(); + this._createCommentForm(focus); } this._createAdditionalActions(); @@ -272,7 +272,7 @@ export class CommentThreadWidget extends this._commentReply.updateCanReply(); } else { if (this._commentThread.canReply) { - this._createCommentForm(); + this._createCommentForm(false); } } })); @@ -286,7 +286,7 @@ export class CommentThreadWidget extends })); } - private _createCommentForm() { + private _createCommentForm(focus: boolean) { this._commentReply = this._scopedInstantiationService.createInstance( CommentReply, this._owner, @@ -299,6 +299,7 @@ export class CommentThreadWidget extends this._commentOptions, this._pendingComment, this, + focus, this._containerDelegate.actionRunner ); diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts index 6ce5527186e..7a6edd60c6d 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts @@ -356,13 +356,13 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this._commentThreadWidget.layout(widthInPixel); } - async display(range: IRange | undefined) { + async display(range: IRange | undefined, shouldReveal: boolean) { if (range) { this._commentGlyph = new CommentGlyphWidget(this.editor, range?.endLineNumber ?? -1); this._commentGlyph.setThreadState(this._commentThread.state); } - await this._commentThreadWidget.display(this.editor.getOption(EditorOption.lineHeight)); + await this._commentThreadWidget.display(this.editor.getOption(EditorOption.lineHeight), shouldReveal); this._disposables.add(this._commentThreadWidget.onDidResize(dimension => { this._refresh(dimension); })); @@ -371,7 +371,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } // If this is a new comment thread awaiting user input then we need to reveal it. - if (this._commentThread.canReply && this._commentThread.isTemplate && (!this._commentThread.comments || (this._commentThread.comments.length === 0))) { + if (shouldReveal) { this.reveal(); } @@ -463,10 +463,6 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this._viewZone.afterLineNumber = currentPosition.lineNumber; } - if (!this._commentThread.comments || !this._commentThread.comments.length) { - this._commentThreadWidget.focusCommentEditor(); - } - const capture = StableEditorScrollState.capture(this.editor); this._relayout(computedLinesNumber); capture.restore(this.editor); diff --git a/src/vs/workbench/contrib/comments/browser/commentsController.ts b/src/vs/workbench/contrib/comments/browser/commentsController.ts index 5a847e234c7..5ba6ca59e04 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsController.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsController.ts @@ -838,6 +838,40 @@ export class CommentController implements IEditorContribution { } } + private async handleCommentAdded(editorId: string | undefined, uniqueOwner: string, thread: languages.AddedCommentThread): Promise { + const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.uniqueOwner === uniqueOwner && zoneWidget.commentThread.threadId === thread.threadId); + if (matchedZones.length) { + return; + } + + const matchedNewCommentThreadZones = this._commentWidgets.filter(zoneWidget => zoneWidget.uniqueOwner === uniqueOwner && zoneWidget.commentThread.commentThreadHandle === -1 && Range.equalsRange(zoneWidget.commentThread.range, thread.range)); + + if (matchedNewCommentThreadZones.length) { + matchedNewCommentThreadZones[0].update(thread); + return; + } + + const continueOnCommentIndex = this._inProcessContinueOnComments.get(uniqueOwner)?.findIndex(pending => { + if (pending.range === undefined) { + return thread.range === undefined; + } else { + return Range.lift(pending.range).equalsRange(thread.range); + } + }); + let continueOnCommentText: string | undefined; + if ((continueOnCommentIndex !== undefined) && continueOnCommentIndex >= 0) { + continueOnCommentText = this._inProcessContinueOnComments.get(uniqueOwner)?.splice(continueOnCommentIndex, 1)[0].body; + } + + const pendingCommentText = (this._pendingNewCommentCache[uniqueOwner] && this._pendingNewCommentCache[uniqueOwner][thread.threadId]) + ?? continueOnCommentText; + const pendingEdits = this._pendingEditsCache[uniqueOwner] && this._pendingEditsCache[uniqueOwner][thread.threadId]; + const shouldReveal = thread.canReply && thread.isTemplate && (!thread.comments || (thread.comments.length === 0)) && (!thread.editorId || (thread.editorId === editorId)); + await this.displayCommentThread(uniqueOwner, thread, shouldReveal, pendingCommentText, pendingEdits); + this._commentInfos.filter(info => info.uniqueOwner === uniqueOwner)[0].threads.push(thread); + this.tryUpdateReservedSpace(); + } + public onModelChanged(): void { this.localToDispose.clear(); this.tryUpdateReservedSpace(); @@ -903,45 +937,17 @@ export class CommentController implements IEditorContribution { } }); - changed.forEach(thread => { + for (const thread of changed) { const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.uniqueOwner === e.uniqueOwner && zoneWidget.commentThread.threadId === thread.threadId); if (matchedZones.length) { const matchedZone = matchedZones[0]; matchedZone.update(thread); this.openCommentsView(thread); } - }); + } + const editorId = this.editor?.getId(); for (const thread of added) { - const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.uniqueOwner === e.uniqueOwner && zoneWidget.commentThread.threadId === thread.threadId); - if (matchedZones.length) { - return; - } - - const matchedNewCommentThreadZones = this._commentWidgets.filter(zoneWidget => zoneWidget.uniqueOwner === e.uniqueOwner && zoneWidget.commentThread.commentThreadHandle === -1 && Range.equalsRange(zoneWidget.commentThread.range, thread.range)); - - if (matchedNewCommentThreadZones.length) { - matchedNewCommentThreadZones[0].update(thread); - return; - } - - const continueOnCommentIndex = this._inProcessContinueOnComments.get(e.uniqueOwner)?.findIndex(pending => { - if (pending.range === undefined) { - return thread.range === undefined; - } else { - return Range.lift(pending.range).equalsRange(thread.range); - } - }); - let continueOnCommentText: string | undefined; - if ((continueOnCommentIndex !== undefined) && continueOnCommentIndex >= 0) { - continueOnCommentText = this._inProcessContinueOnComments.get(e.uniqueOwner)?.splice(continueOnCommentIndex, 1)[0].body; - } - - const pendingCommentText = (this._pendingNewCommentCache[e.uniqueOwner] && this._pendingNewCommentCache[e.uniqueOwner][thread.threadId]) - ?? continueOnCommentText; - const pendingEdits = this._pendingEditsCache[e.uniqueOwner] && this._pendingEditsCache[e.uniqueOwner][thread.threadId]; - await this.displayCommentThread(e.uniqueOwner, thread, pendingCommentText, pendingEdits); - this._commentInfos.filter(info => info.uniqueOwner === e.uniqueOwner)[0].threads.push(thread); - this.tryUpdateReservedSpace(); + await this.handleCommentAdded(editorId, e.uniqueOwner, thread); } for (const thread of pending) { @@ -1020,7 +1026,7 @@ export class CommentController implements IEditorContribution { return undefined; } - private async displayCommentThread(uniqueOwner: string, thread: languages.CommentThread, pendingComment: string | undefined, pendingEdits: { [key: number]: string } | undefined): Promise { + private async displayCommentThread(uniqueOwner: string, thread: languages.CommentThread, shouldReveal: boolean, pendingComment: string | undefined, pendingEdits: { [key: number]: string } | undefined): Promise { const editor = this.editor?.getModel(); if (!editor) { return; @@ -1034,7 +1040,7 @@ export class CommentController implements IEditorContribution { continueOnCommentReply = this.commentService.removeContinueOnComment({ uniqueOwner, uri: editor.uri, range: thread.range, isReply: true }); } const zoneWidget = this.instantiationService.createInstance(ReviewZoneWidget, this.editor, uniqueOwner, thread, pendingComment ?? continueOnCommentReply?.body, pendingEdits); - await zoneWidget.display(thread.range); + await zoneWidget.display(thread.range, shouldReveal); this._commentWidgets.push(zoneWidget); this.openCommentsView(thread); } @@ -1202,7 +1208,7 @@ export class CommentController implements IEditorContribution { if (!this.editor) { return; } - this.commentService.createCommentThreadTemplate(ownerId, this.editor.getModel()!.uri, range); + this.commentService.createCommentThreadTemplate(ownerId, this.editor.getModel()!.uri, range, this.editor.getId()); this.processNextThreadToAdd(); return; } @@ -1325,7 +1331,7 @@ export class CommentController implements IEditorContribution { pendingEdits = providerEditsCacheStore[thread.threadId]; } - await this.displayCommentThread(info.uniqueOwner, thread, pendingComment, pendingEdits); + await this.displayCommentThread(info.uniqueOwner, thread, false, pendingComment, pendingEdits); } for (const thread of info.pendingCommentThreads ?? []) { this.resumePendingComment(this.editor!.getModel()!.uri, thread); diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellComments.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellComments.ts index 86682ca341d..b8ccbf07abf 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellComments.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellComments.ts @@ -84,7 +84,7 @@ export class CellComments extends CellContentPart { const layoutInfo = this.notebookEditor.getLayoutInfo(); - await this._commentThreadWidget.display(layoutInfo.fontInfo.lineHeight); + await this._commentThreadWidget.display(layoutInfo.fontInfo.lineHeight, true); this._applyTheme(); this.commentTheadDisposables.add(this._commentThreadWidget.onDidResize(() => { From 71718e2b96e5add20afe66dc628e6f9ae5b70a8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EB=8F=99=EC=9C=A4=20=28Donny=29?= Date: Tue, 7 May 2024 18:28:22 +0900 Subject: [PATCH 015/357] json schema for swcrc --- extensions/typescript-language-features/package.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index fb5383a2978..3d97ecafc6c 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -140,6 +140,10 @@ "fileMatch": "jsconfig.*.json", "url": "./schemas/jsconfig.schema.json" }, + { + "fileMatch": ".swcrc", + "url": "https://swc.rs/schema.json" + }, { "fileMatch": "typedoc.json", "url": "https://typedoc.org/schema.json" From f8c7fec0c29c1f6d2fd65c3b2c779d362fe61d0b Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 7 May 2024 12:36:52 +0200 Subject: [PATCH 016/357] More extension install error codes (#212161) --- .../abstractExtensionManagementService.ts | 90 ++++---- .../common/extensionGalleryService.ts | 48 ++-- .../common/extensionManagement.ts | 36 +-- .../node/extensionDownloader.ts | 32 +-- .../node/extensionManagementService.ts | 213 ++++++++++-------- 5 files changed, 236 insertions(+), 183 deletions(-) diff --git a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts index 55a1e34ba60..95438e4a764 100644 --- a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts +++ b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts @@ -17,7 +17,7 @@ import { ExtensionManagementError, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementParticipant, IGalleryExtension, ILocalExtension, InstallOperation, IExtensionsControlManifest, StatisticType, isTargetPlatformCompatible, TargetPlatformToString, ExtensionManagementErrorCode, InstallOptions, UninstallOptions, Metadata, InstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, IExtensionManagementService, InstallExtensionInfo, EXTENSION_INSTALL_DEP_PACK_CONTEXT, ExtensionGalleryError, - IProductVersion + IProductVersion, ExtensionGalleryErrorCode } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions, ExtensionKey, getGalleryExtensionId, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionType, IExtensionManifest, isApplicationScopedExtension, TargetPlatform } from 'vs/platform/extensions/common/extensions'; @@ -290,26 +290,10 @@ export abstract class AbstractExtensionManagementService extends Disposable impl // Install extensions in parallel and wait until all extensions are installed / failed await this.joinAllSettled([...installingExtensionsMap.entries()].map(async ([key, { task }]) => { const startTime = new Date().getTime(); + let local: ILocalExtension; try { - const local = await task.run(); - await this.joinAllSettled(this.participants.map(participant => participant.postInstall(local, task.source, task.options, CancellationToken.None))); - if (!URI.isUri(task.source)) { - const isUpdate = task.operation === InstallOperation.Update; - const durationSinceUpdate = isUpdate ? undefined : (new Date().getTime() - task.source.lastUpdated) / 1000; - reportTelemetry(this.telemetryService, isUpdate ? 'extensionGallery:update' : 'extensionGallery:install', { - extensionData: getGalleryExtensionTelemetryData(task.source), - verificationStatus: task.verificationStatus, - duration: new Date().getTime() - startTime, - durationSinceUpdate - }); - // In web, report extension install statistics explicitly. In Desktop, statistics are automatically updated while downloading the VSIX. - if (isWeb && task.operation !== InstallOperation.Update) { - try { - await this.galleryService.reportStatistic(local.manifest.publisher, local.manifest.name, local.manifest.version, StatisticType.Install); - } catch (error) { /* ignore */ } - } - } - installExtensionResultsMap.set(key, { local, identifier: task.identifier, operation: task.operation, source: task.source, context: task.options.context, profileLocation: task.profileLocation, applicationScoped: local.isApplicationScoped }); + local = await task.run(); + await this.joinAllSettled(this.participants.map(participant => participant.postInstall(local, task.source, task.options, CancellationToken.None)), ExtensionManagementErrorCode.PostInstall); } catch (e) { const error = toExtensionManagementError(e); if (!URI.isUri(task.source)) { @@ -319,6 +303,23 @@ export abstract class AbstractExtensionManagementService extends Disposable impl this.logService.error('Error while installing the extension', task.identifier.id, getErrorMessage(error)); throw error; } + if (!URI.isUri(task.source)) { + const isUpdate = task.operation === InstallOperation.Update; + const durationSinceUpdate = isUpdate ? undefined : (new Date().getTime() - task.source.lastUpdated) / 1000; + reportTelemetry(this.telemetryService, isUpdate ? 'extensionGallery:update' : 'extensionGallery:install', { + extensionData: getGalleryExtensionTelemetryData(task.source), + verificationStatus: task.verificationStatus, + duration: new Date().getTime() - startTime, + durationSinceUpdate + }); + // In web, report extension install statistics explicitly. In Desktop, statistics are automatically updated while downloading the VSIX. + if (isWeb && task.operation !== InstallOperation.Update) { + try { + await this.galleryService.reportStatistic(local.manifest.publisher, local.manifest.name, local.manifest.version, StatisticType.Install); + } catch (error) { /* ignore */ } + } + } + installExtensionResultsMap.set(key, { local, identifier: task.identifier, operation: task.operation, source: task.source, context: task.options.context, profileLocation: task.profileLocation, applicationScoped: local.isApplicationScoped }); })); if (alreadyRequestedInstallations.length) { @@ -428,36 +429,35 @@ export abstract class AbstractExtensionManagementService extends Disposable impl return true; } - private async joinAllSettled(promises: Promise[]): Promise { + private async joinAllSettled(promises: Promise[], errorCode?: ExtensionManagementErrorCode): Promise { const results: T[] = []; - const errors: any[] = []; + const errors: ExtensionManagementError[] = []; const promiseResults = await Promise.allSettled(promises); for (const r of promiseResults) { if (r.status === 'fulfilled') { results.push(r.value); } else { - errors.push(r.reason); + errors.push(toExtensionManagementError(r.reason, errorCode)); } } + if (!errors.length) { + return results; + } + // Throw if there are errors - if (errors.length) { - if (errors.length === 1) { - throw errors[0]; - } - - let error = new ExtensionManagementError('', ExtensionManagementErrorCode.Unknown); - for (const current of errors) { - const code = current instanceof ExtensionManagementError ? current.code : ExtensionManagementErrorCode.Unknown; - error = new ExtensionManagementError( - current.message ? `${current.message}, ${error.message}` : error.message, - code !== ExtensionManagementErrorCode.Unknown && code !== ExtensionManagementErrorCode.Internal ? code : error.code - ); - } - throw error; + if (errors.length === 1) { + throw errors[0]; } - return results; + let error = new ExtensionManagementError('', ExtensionManagementErrorCode.Unknown); + for (const current of errors) { + error = new ExtensionManagementError( + error.message ? `${error.message}, ${current.message}` : current.message, + current.code !== ExtensionManagementErrorCode.Unknown && current.code !== ExtensionManagementErrorCode.Internal ? current.code : error.code + ); + } + throw error; } private async getAllDepsAndPackExtensions(extensionIdentifier: IExtensionIdentifier, manifest: IExtensionManifest, getOnlyNewlyAddedFromExtensionPack: boolean, installPreRelease: boolean, profile: URI | undefined, productVersion: IProductVersion): Promise<{ gallery: IGalleryExtension; manifest: IExtensionManifest }[]> { @@ -787,18 +787,18 @@ export abstract class AbstractExtensionManagementService extends Disposable impl protected abstract copyExtension(extension: ILocalExtension, fromProfileLocation: URI, toProfileLocation: URI, metadata?: Partial): Promise; } -export function toExtensionManagementError(error: Error): ExtensionManagementError { +export function toExtensionManagementError(error: Error, code?: ExtensionManagementErrorCode): ExtensionManagementError { if (error instanceof ExtensionManagementError) { return error; } + let extensionManagementError: ExtensionManagementError; if (error instanceof ExtensionGalleryError) { - const e = new ExtensionManagementError(error.message, ExtensionManagementErrorCode.Gallery); - e.stack = error.stack; - return e; + extensionManagementError = new ExtensionManagementError(error.message, error.code === ExtensionGalleryErrorCode.DownloadFailedWriting ? ExtensionManagementErrorCode.DownloadFailedWriting : ExtensionManagementErrorCode.Gallery); + } else { + extensionManagementError = new ExtensionManagementError(error.message, isCancellationError(error) ? ExtensionManagementErrorCode.Cancelled : (code ?? ExtensionManagementErrorCode.Internal)); } - const e = new ExtensionManagementError(error.message, isCancellationError(error) ? ExtensionManagementErrorCode.Cancelled : ExtensionManagementErrorCode.Internal); - e.stack = error.stack; - return e; + extensionManagementError.stack = error.stack; + return extensionManagementError; } function reportTelemetry(telemetryService: ITelemetryService, eventName: string, { extensionData, verificationStatus, duration, error, durationSinceUpdate }: { extensionData: any; verificationStatus?: ExtensionVerificationStatus; duration?: number; durationSinceUpdate?: number; error?: ExtensionManagementError | ExtensionGalleryError }): void { diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index ebdb1bb50b8..cc587b5770e 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -1029,16 +1029,6 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi this.logService.trace('ExtensionGalleryService#download', extension.identifier.id); const data = getGalleryExtensionTelemetryData(extension); const startTime = new Date().getTime(); - /* __GDPR__ - "galleryService:downloadVSIX" : { - "owner": "sandy081", - "duration": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "${include}": [ - "${GalleryExtensionTelemetryData}" - ] - } - */ - const log = (duration: number) => this.telemetryService.publicLog('galleryService:downloadVSIX', { ...data, duration }); const operationParam = operation === InstallOperation.Install ? 'install' : operation === InstallOperation.Update ? 'update' : ''; const downloadAsset = operationParam ? { @@ -1048,8 +1038,29 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi const headers: IHeaders | undefined = extension.queryContext?.[ACTIVITY_HEADER_NAME] ? { [ACTIVITY_HEADER_NAME]: extension.queryContext[ACTIVITY_HEADER_NAME] } : undefined; const context = await this.getAsset(extension.identifier.id, downloadAsset, AssetType.VSIX, headers ? { headers } : undefined); - await this.fileService.writeFile(location, context.stream); - log(new Date().getTime() - startTime); + + try { + await this.fileService.writeFile(location, context.stream); + } catch (error) { + try { + await this.fileService.del(location); + } catch (e) { + /* ignore */ + this.logService.warn(`Error while deleting the file ${location.toString()}`, getErrorMessage(e)); + } + throw new ExtensionGalleryError(getErrorMessage(error), ExtensionGalleryErrorCode.DownloadFailedWriting); + } + + /* __GDPR__ + "galleryService:downloadVSIX" : { + "owner": "sandy081", + "duration": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "${include}": [ + "${GalleryExtensionTelemetryData}" + ] + } + */ + this.telemetryService.publicLog('galleryService:downloadVSIX', { ...data, duration: new Date().getTime() - startTime }); } async downloadSignatureArchive(extension: IGalleryExtension, location: URI): Promise { @@ -1060,7 +1071,18 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi this.logService.trace('ExtensionGalleryService#downloadSignatureArchive', extension.identifier.id); const context = await this.getAsset(extension.identifier.id, extension.assets.signature, AssetType.Signature); - await this.fileService.writeFile(location, context.stream); + try { + await this.fileService.writeFile(location, context.stream); + } catch (error) { + try { + await this.fileService.del(location); + } catch (e) { + /* ignore */ + this.logService.warn(`Error while deleting the file ${location.toString()}`, getErrorMessage(e)); + } + throw new ExtensionGalleryError(getErrorMessage(error), ExtensionGalleryErrorCode.DownloadFailedWriting); + } + } async getReadme(extension: IGalleryExtension, token: CancellationToken): Promise { diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index d69233901b8..fa9e1b0983d 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -407,7 +407,21 @@ export interface DidUninstallExtensionEvent { readonly workspaceScoped?: boolean; } -export enum ExtensionManagementErrorCode { +export const enum ExtensionGalleryErrorCode { + Timeout = 'Timeout', + Cancelled = 'Cancelled', + Failed = 'Failed', + DownloadFailedWriting = 'DownloadFailedWriting', +} + +export class ExtensionGalleryError extends Error { + constructor(message: string, readonly code: ExtensionGalleryErrorCode) { + super(message); + this.name = code; + } +} + +export const enum ExtensionManagementErrorCode { Unsupported = 'Unsupported', Deprecated = 'Deprecated', Malicious = 'Malicious', @@ -417,11 +431,18 @@ export enum ExtensionManagementErrorCode { Invalid = 'Invalid', Download = 'Download', DownloadSignature = 'DownloadSignature', + DownloadFailedWriting = ExtensionGalleryErrorCode.DownloadFailedWriting, UpdateMetadata = 'UpdateMetadata', Extract = 'Extract', Scanning = 'Scanning', + ScanningExtension = 'ScanningExtension', + ReadUninstalled = 'ReadUninstalled', + UnsetUninstalled = 'UnsetUninstalled', Delete = 'Delete', Rename = 'Rename', + IntializeDefaultProfile = 'IntializeDefaultProfile', + AddToProfile = 'AddToProfile', + PostInstall = 'PostInstall', CorruptZip = 'CorruptZip', IncompleteZip = 'IncompleteZip', Signature = 'Signature', @@ -439,19 +460,6 @@ export class ExtensionManagementError extends Error { } } -export enum ExtensionGalleryErrorCode { - Timeout = 'Timeout', - Cancelled = 'Cancelled', - Failed = 'Failed' -} - -export class ExtensionGalleryError extends Error { - constructor(message: string, readonly code: ExtensionGalleryErrorCode) { - super(message); - this.name = code; - } -} - export type InstallOptions = { isBuiltin?: boolean; isWorkspaceScoped?: boolean; diff --git a/src/vs/platform/extensionManagement/node/extensionDownloader.ts b/src/vs/platform/extensionManagement/node/extensionDownloader.ts index 85fb4668e79..d12dc956d44 100644 --- a/src/vs/platform/extensionManagement/node/extensionDownloader.ts +++ b/src/vs/platform/extensionManagement/node/extensionDownloader.ts @@ -16,7 +16,7 @@ import { Promises as FSPromises } from 'vs/base/node/pfs'; import { CorruptZipMessage } from 'vs/base/node/zip'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; -import { ExtensionVerificationStatus } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService'; +import { ExtensionVerificationStatus, toExtensionManagementError } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService'; import { ExtensionManagementError, ExtensionManagementErrorCode, IExtensionGalleryService, IGalleryExtension, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionKey, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionSignatureVerificationError, ExtensionSignatureVerificationCode, IExtensionSignatureVerificationService } from 'vs/platform/extensionManagement/node/extensionSignatureVerificationService'; @@ -52,7 +52,7 @@ export class ExtensionsDownloader extends Disposable { try { await this.downloadFile(extension, location, location => this.extensionGalleryService.download(extension, location, operation)); } catch (error) { - throw new ExtensionManagementError(error.message, ExtensionManagementErrorCode.Download); + throw toExtensionManagementError(error, ExtensionManagementErrorCode.Download); } let verificationStatus: ExtensionVerificationStatus = false; @@ -62,23 +62,20 @@ export class ExtensionsDownloader extends Disposable { try { verificationStatus = await this.extensionSignatureVerificationService.verify(extension.identifier.id, location.fsPath, signatureArchiveLocation.fsPath); } catch (error) { - const sigError = error as ExtensionSignatureVerificationError; - verificationStatus = sigError.code; + verificationStatus = (error as ExtensionSignatureVerificationError).code; if (verificationStatus === ExtensionSignatureVerificationCode.PackageIsInvalidZip || verificationStatus === ExtensionSignatureVerificationCode.SignatureArchiveIsInvalidZip) { - try { - // Delete the downloaded vsix before throwing the error - await this.delete(location); - } catch (error) { - this.logService.error(error); - } throw new ExtensionManagementError(CorruptZipMessage, ExtensionManagementErrorCode.CorruptZip); } } finally { try { - // Delete signature archive always - await this.delete(signatureArchiveLocation); + // Delete downloaded files + await Promise.allSettled([ + this.delete(location), + this.delete(signatureArchiveLocation) + ]); } catch (error) { - this.logService.error(error); + // Ignore error + this.logService.warn(`Error while deleting downloaded files: ${getErrorMessage(error)}`); } } } @@ -103,7 +100,7 @@ export class ExtensionsDownloader extends Disposable { try { await this.downloadFile(extension, location, location => this.extensionGalleryService.downloadSignatureArchive(extension, location)); } catch (error) { - throw new ExtensionManagementError(error.message, ExtensionManagementErrorCode.DownloadSignature); + throw toExtensionManagementError(error, ExtensionManagementErrorCode.DownloadSignature); } return location; } @@ -122,8 +119,13 @@ export class ExtensionsDownloader extends Disposable { // Download to temporary location first only if file does not exist const tempLocation = joinPath(this.extensionsDownloadDir, `.${generateUuid()}`); - if (!await this.fileService.exists(tempLocation)) { + try { await downloadFn(tempLocation); + } catch (error) { + try { + await this.fileService.del(tempLocation); + } catch (e) { /* ignore */ } + throw error; } try { diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 53630b739fe..122a6ed9b7b 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -451,20 +451,28 @@ export class ExtensionsScanner extends Disposable { } async scanExtensions(type: ExtensionType | null, profileLocation: URI, productVersion: IProductVersion): Promise { - const userScanOptions: ScanOptions = { includeInvalid: true, profileLocation, productVersion }; - let scannedExtensions: IScannedExtension[] = []; - if (type === null || type === ExtensionType.System) { - scannedExtensions.push(...await this.extensionsScannerService.scanAllExtensions({ includeInvalid: true }, userScanOptions, false)); - } else if (type === ExtensionType.User) { - scannedExtensions.push(...await this.extensionsScannerService.scanUserExtensions(userScanOptions)); + try { + const userScanOptions: ScanOptions = { includeInvalid: true, profileLocation, productVersion }; + let scannedExtensions: IScannedExtension[] = []; + if (type === null || type === ExtensionType.System) { + scannedExtensions.push(...await this.extensionsScannerService.scanAllExtensions({ includeInvalid: true }, userScanOptions, false)); + } else if (type === ExtensionType.User) { + scannedExtensions.push(...await this.extensionsScannerService.scanUserExtensions(userScanOptions)); + } + scannedExtensions = type !== null ? scannedExtensions.filter(r => r.type === type) : scannedExtensions; + return await Promise.all(scannedExtensions.map(extension => this.toLocalExtension(extension))); + } catch (error) { + throw toExtensionManagementError(error, ExtensionManagementErrorCode.Scanning); } - scannedExtensions = type !== null ? scannedExtensions.filter(r => r.type === type) : scannedExtensions; - return Promise.all(scannedExtensions.map(extension => this.toLocalExtension(extension))); } async scanAllUserExtensions(excludeOutdated: boolean): Promise { - const scannedExtensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: !excludeOutdated, includeInvalid: true }); - return Promise.all(scannedExtensions.map(extension => this.toLocalExtension(extension))); + try { + const scannedExtensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: !excludeOutdated, includeInvalid: true }); + return await Promise.all(scannedExtensions.map(extension => this.toLocalExtension(extension))); + } catch (error) { + throw toExtensionManagementError(error, ExtensionManagementErrorCode.Scanning); + } } async scanUserExtensionAtLocation(location: URI): Promise { @@ -496,7 +504,11 @@ export class ExtensionsScanner extends Disposable { } if (exists) { - await this.extensionsScannerService.updateMetadata(extensionLocation, metadata); + try { + await this.extensionsScannerService.updateMetadata(extensionLocation, metadata); + } catch (error) { + throw toExtensionManagementError(error, ExtensionManagementErrorCode.UpdateMetadata); + } } else { try { // Extract @@ -513,10 +525,14 @@ export class ExtensionsScanner extends Disposable { errorCode = ExtensionManagementErrorCode.IncompleteZip; } } - throw new ExtensionManagementError(e.message, errorCode); + throw toExtensionManagementError(e, errorCode); } - await this.extensionsScannerService.updateMetadata(tempLocation, metadata); + try { + await this.extensionsScannerService.updateMetadata(tempLocation, metadata); + } catch (error) { + throw toExtensionManagementError(error, ExtensionManagementErrorCode.UpdateMetadata); + } // Rename try { @@ -558,16 +574,24 @@ export class ExtensionsScanner extends Disposable { } async updateMetadata(local: ILocalExtension, metadata: Partial, profileLocation?: URI): Promise { - if (profileLocation) { - await this.extensionsProfileScannerService.updateMetadata([[local, metadata]], profileLocation); - } else { - await this.extensionsScannerService.updateMetadata(local.location, metadata); + try { + if (profileLocation) { + await this.extensionsProfileScannerService.updateMetadata([[local, metadata]], profileLocation); + } else { + await this.extensionsScannerService.updateMetadata(local.location, metadata); + } + } catch (error) { + throw toExtensionManagementError(error, ExtensionManagementErrorCode.UpdateMetadata); } return this.scanLocalExtension(local.location, local.type, profileLocation); } - getUninstalledExtensions(): Promise> { - return this.withUninstalledExtensions(); + async getUninstalledExtensions(): Promise> { + try { + return await this.withUninstalledExtensions(); + } catch (error) { + throw toExtensionManagementError(error, ExtensionManagementErrorCode.ReadUninstalled); + } } async setUninstalled(...extensions: IExtension[]): Promise { @@ -580,7 +604,11 @@ export class ExtensionsScanner extends Disposable { } async setInstalled(extensionKey: ExtensionKey): Promise { - await this.withUninstalledExtensions(uninstalled => delete uninstalled[extensionKey.toString()]); + try { + await this.withUninstalledExtensions(uninstalled => delete uninstalled[extensionKey.toString()]); + } catch (error) { + throw toExtensionManagementError(error, ExtensionManagementErrorCode.UnsetUninstalled); + } } async removeExtension(extension: ILocalExtension | IScannedExtension, type: string): Promise { @@ -630,7 +658,7 @@ export class ExtensionsScanner extends Disposable { this.logService.info(`Deleted ${type} extension from disk`, id, location.fsPath); } - private async withUninstalledExtensions(updateFn?: (uninstalled: IStringDictionary) => void): Promise> { + private withUninstalledExtensions(updateFn?: (uninstalled: IStringDictionary) => void): Promise> { return this.uninstalledFileLimiter.queue(async () => { let raw: string | undefined; try { @@ -666,24 +694,28 @@ export class ExtensionsScanner extends Disposable { try { await pfs.Promises.rename(extractPath, renamePath, 2 * 60 * 1000 /* Retry for 2 minutes */); } catch (error) { - throw new ExtensionManagementError(error.message || nls.localize('renameError', "Unknown error while renaming {0} to {1}", extractPath, renamePath), error.code || ExtensionManagementErrorCode.Rename); + throw toExtensionManagementError(error, ExtensionManagementErrorCode.Rename); } } private async scanLocalExtension(location: URI, type: ExtensionType, profileLocation?: URI): Promise { - if (profileLocation) { - const scannedExtensions = await this.extensionsScannerService.scanUserExtensions({ profileLocation }); - const scannedExtension = scannedExtensions.find(e => this.uriIdentityService.extUri.isEqual(e.location, location)); - if (scannedExtension) { - return this.toLocalExtension(scannedExtension); - } - } else { - const scannedExtension = await this.extensionsScannerService.scanExistingExtension(location, type, { includeInvalid: true }); - if (scannedExtension) { - return this.toLocalExtension(scannedExtension); + try { + if (profileLocation) { + const scannedExtensions = await this.extensionsScannerService.scanUserExtensions({ profileLocation }); + const scannedExtension = scannedExtensions.find(e => this.uriIdentityService.extUri.isEqual(e.location, location)); + if (scannedExtension) { + return await this.toLocalExtension(scannedExtension); + } + } else { + const scannedExtension = await this.extensionsScannerService.scanExistingExtension(location, type, { includeInvalid: true }); + if (scannedExtension) { + return await this.toLocalExtension(scannedExtension); + } } + throw new ExtensionManagementError(nls.localize('cannot read', "Cannot read the extension from {0}", location.path), ExtensionManagementErrorCode.ScanningExtension); + } catch (error) { + throw toExtensionManagementError(error, ExtensionManagementErrorCode.ScanningExtension); } - throw new Error(nls.localize('cannot read', "Cannot read the extension from {0}", location.path)); } private async toLocalExtension(extension: IScannedExtension): Promise { @@ -821,9 +853,17 @@ abstract class InstallExtensionTask extends AbstractExtensionTask { - const isUninstalled = await this.isUninstalled(extensionKey); - if (!isUninstalled) { + const uninstalled = await this.extensionsScanner.getUninstalledExtensions(); + if (!uninstalled[extensionKey.toString()]) { return undefined; } @@ -854,11 +894,6 @@ abstract class InstallExtensionTask extends AbstractExtensionTask ExtensionKey.create(i).equals(extensionKey)); } - private async isUninstalled(extensionId: ExtensionKey): Promise { - const uninstalled = await this.extensionsScanner.getUninstalledExtensions(); - return !!uninstalled[extensionId.toString()]; - } - protected abstract install(token: CancellationToken): Promise<[ILocalExtension, Metadata]>; } @@ -882,13 +917,7 @@ export class InstallGalleryExtensionTask extends InstallExtensionTask { } protected async install(token: CancellationToken): Promise<[ILocalExtension, Metadata]> { - let installed; - try { - installed = await this.extensionsScanner.scanExtensions(null, this.options.profileLocation, this.options.productVersion); - } catch (error) { - throw new ExtensionManagementError(error, ExtensionManagementErrorCode.Scanning); - } - + const installed = await this.extensionsScanner.scanExtensions(null, this.options.profileLocation, this.options.productVersion); const existingExtension = installed.find(i => areSameExtensions(i.identifier, this.gallery.identifier)); if (existingExtension) { this._operation = InstallOperation.Update; @@ -915,72 +944,64 @@ export class InstallGalleryExtensionTask extends InstallExtensionTask { }; if (existingExtension && existingExtension.type !== ExtensionType.System && existingExtension.manifest.version === this.gallery.version) { - try { - const local = await this.extensionsScanner.updateMetadata(existingExtension, metadata); - return [local, metadata]; - } catch (error) { - throw new ExtensionManagementError(getErrorMessage(error), ExtensionManagementErrorCode.UpdateMetadata); - } + const local = await this.extensionsScanner.updateMetadata(existingExtension, metadata); + return [local, metadata]; } - try { - return await this.downloadAndInstallExtension(metadata, token); - } catch (error) { - if (error instanceof ExtensionManagementError && (error.code === ExtensionManagementErrorCode.CorruptZip || error.code === ExtensionManagementErrorCode.IncompleteZip)) { - this.logService.info(`Downloaded VSIX is invalid. Trying to download and install again...`, this.gallery.identifier.id); - type RetryInstallingInvalidVSIXClassification = { - owner: 'sandy081'; - comment: 'Event reporting the retry of installing an invalid VSIX'; - extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Extension Id' }; - succeeded?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Success value' }; - }; - type RetryInstallingInvalidVSIXEvent = { - extensionId: string; - succeeded: boolean; - }; - try { - const result = await this.downloadAndInstallExtension(metadata, token); - this.telemetryService.publicLog2('extensiongallery:install:retry', { - extensionId: this.gallery.identifier.id, - succeeded: true - }); - return result; - } catch (error) { - this.telemetryService.publicLog2('extensiongallery:install:retry', { - extensionId: this.gallery.identifier.id, - succeeded: false - }); - throw error; - } - } else { - throw error; - } - } - } - - private async downloadAndInstallExtension(metadata: Metadata, token: CancellationToken): Promise<[ILocalExtension, Metadata]> { - const { location, verificationStatus } = await this.extensionsDownloader.download(this.gallery, this._operation, !this.options.donotVerifySignature); + const { verificationStatus, location } = await this.download(metadata, token); try { this._verificationStatus = verificationStatus; - this.validateManifest(location.fsPath); + await this.validateManifest(location.fsPath); const local = await this.extractExtension({ zipPath: location.fsPath, key: ExtensionKey.create(this.gallery), metadata }, false, token); return [local, metadata]; } catch (error) { try { await this.extensionsDownloader.delete(location); - } catch (error) { + } catch (e) { /* Ignore */ - this.logService.warn(`Error while deleting the downloaded file`, location.toString(), getErrorMessage(error)); + this.logService.warn(`Error while deleting the downloaded file`, location.toString(), getErrorMessage(e)); } throw error; } } + private async download(metadata: Metadata, token: CancellationToken): Promise<{ readonly location: URI; readonly verificationStatus: ExtensionVerificationStatus }> { + try { + return await this.extensionsDownloader.download(this.gallery, this._operation, !this.options.donotVerifySignature); + } catch (error) { + this.logService.info(`Failed downloading. Retry again...`, this.gallery.identifier.id); + type RetryDownloadingVSIXClassification = { + owner: 'sandy081'; + comment: 'Event reporting the retry of downloading the VSIX'; + extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Extension Id' }; + succeeded?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Success value' }; + }; + type RetryDownloadingVSIXEvent = { + extensionId: string; + succeeded: boolean; + }; + try { + const result = await this.download(metadata, token); + this.telemetryService.publicLog2('extensiongallery:download:retry', { + extensionId: this.gallery.identifier.id, + succeeded: true + }); + return result; + } catch (error) { + this.telemetryService.publicLog2('extensiongallery:download:retry', { + extensionId: this.gallery.identifier.id, + succeeded: false + }); + throw error; + } + } + } + protected async validateManifest(zipPath: string): Promise { try { await getManifest(zipPath); } catch (error) { - throw new ExtensionManagementError(error.message, ExtensionManagementErrorCode.Invalid); + throw toExtensionManagementError(error, ExtensionManagementErrorCode.Invalid); } } From 45ae3b3eace33e03c301b5c74a99763ad2ff4aa6 Mon Sep 17 00:00:00 2001 From: OccasionalDebugger <168763641+OccasionalDebugger@users.noreply.github.com> Date: Thu, 2 May 2024 16:49:05 +0000 Subject: [PATCH 017/357] Respect stackframe deemphasize in getTopStackFrame --- src/vs/workbench/contrib/debug/common/debugModel.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index b12e9b726ff..e8820cf43de 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -22,7 +22,7 @@ import * as nls from 'vs/nls'; import { ILogService } from 'vs/platform/log/common/log'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IEditorPane } from 'vs/workbench/common/editor'; -import { DEBUG_MEMORY_SCHEME, DataBreakpointSetType, DataBreakpointSource, DebugTreeItemCollapsibleState, IBaseBreakpoint, IBreakpoint, IBreakpointData, IBreakpointUpdateData, IBreakpointsChangeEvent, IDataBreakpoint, IDebugModel, IDebugSession, IDebugVisualizationTreeItem, IEnablement, IExceptionBreakpoint, IExceptionInfo, IExpression, IExpressionContainer, IFunctionBreakpoint, IInstructionBreakpoint, IMemoryInvalidationEvent, IMemoryRegion, IRawModelUpdate, IRawStoppedDetails, IScope, IStackFrame, IThread, ITreeElement, MemoryRange, MemoryRangeType, State } from 'vs/workbench/contrib/debug/common/debug'; +import { DEBUG_MEMORY_SCHEME, DataBreakpointSetType, DataBreakpointSource, DebugTreeItemCollapsibleState, IBaseBreakpoint, IBreakpoint, IBreakpointData, IBreakpointUpdateData, IBreakpointsChangeEvent, IDataBreakpoint, IDebugModel, IDebugSession, IDebugVisualizationTreeItem, IEnablement, IExceptionBreakpoint, IExceptionInfo, IExpression, IExpressionContainer, IFunctionBreakpoint, IInstructionBreakpoint, IMemoryInvalidationEvent, IMemoryRegion, IRawModelUpdate, IRawStoppedDetails, IScope, IStackFrame, IThread, ITreeElement, MemoryRange, MemoryRangeType, State, isFrameDeemphasized } from 'vs/workbench/contrib/debug/common/debug'; import { Source, UNKNOWN_SOURCE_LABEL, getUriFromSource } from 'vs/workbench/contrib/debug/common/debugSource'; import { DebugStorage } from 'vs/workbench/contrib/debug/common/debugStorage'; import { IDebugVisualizerService } from 'vs/workbench/contrib/debug/common/debugVisualizers'; @@ -579,9 +579,9 @@ export class Thread implements IThread { getTopStackFrame(): IStackFrame | undefined { const callStack = this.getCallStack(); // Allow stack frame without source and with instructionReferencePointer as top stack frame when using disassembly view. - const firstAvailableStackFrame = callStack.find(sf => !!(sf && + const firstAvailableStackFrame = callStack.find(sf => !!( ((this.stoppedDetails?.reason === 'instruction breakpoint' || (this.stoppedDetails?.reason === 'step' && this.lastSteppingGranularity === 'instruction')) && sf.instructionPointerReference) || - (sf.source && sf.source.available && sf.source.presentationHint !== 'deemphasize'))); + (sf.source && sf.source.available && !isFrameDeemphasized(sf)))); return firstAvailableStackFrame; } From 17bea607709367555a176f49688a12496035d6ac Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Tue, 7 May 2024 16:39:33 +0200 Subject: [PATCH 018/357] Improve description #211931 (#212170) --- src/vs/workbench/browser/workbench.contribution.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index 4824d6c881f..5151e6228eb 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -94,14 +94,14 @@ const registry = Registry.as(ConfigurationExtensions.Con [CustomEditorLabelService.SETTING_ID_PATTERNS]: { 'type': 'object', 'markdownDescription': (() => { - let customEditorLabelDescription = localize('workbench.editor.label.patterns', "Controls the rendering of the editor label. Each __Item__ is a pattern that matches a file path. Both relative and absolute file paths are supported. In case multiple patterns match, the longest matching path will be picked. Each __Value__ is the template for the rendered editor when the __Item__ matches. Variables are substituted based on the context:"); + let customEditorLabelDescription = localize('workbench.editor.label.patterns', "Controls the rendering of the editor label. Each __Item__ is a pattern that matches a file path. Both relative and absolute file paths are supported. The relative path must include the WORKSPACE_FOLDER (e.g `WORKSPACE_FOLDER/src/**.tsx` or `*/src/**.tsx`). Absolute patterns must start with a `/`. In case multiple patterns match, the longest matching path will be picked. Each __Value__ is the template for the rendered editor when the __Item__ matches. Variables are substituted based on the context:"); customEditorLabelDescription += '\n- ' + [ - localize('workbench.editor.label.dirname', "`${dirname}`: name of the folder in which the file is located (e.g. `root/folder/file.txt -> folder`)."), - localize('workbench.editor.label.nthdirname', "`${dirname(N)}`: name of the nth parent folder in which the file is located (e.g. `N=1: root/folder/file.txt -> root`). Folders can be picked from the start of the path by using negative numbers (e.g. `N=-1: root/folder/file.txt -> root`). If the __Item__ is an absolute pattern path, the first folder (`N=-1`) refers to the first folder in the absoulte path, otherwise it corresponds to the workspace folder."), - localize('workbench.editor.label.filename', "`${filename}`: name of the file without the file extension (e.g. `root/folder/file.txt -> file`)."), - localize('workbench.editor.label.extname', "`${extname}`: the file extension (e.g. `root/folder/file.txt -> txt`)."), + localize('workbench.editor.label.dirname', "`${dirname}`: name of the folder in which the file is located (e.g. `WORKSPACE_FOLDER/folder/file.txt -> folder`)."), + localize('workbench.editor.label.nthdirname', "`${dirname(N)}`: name of the nth parent folder in which the file is located (e.g. `N=2: WORKSPACE_FOLDER/static/folder/file.txt -> WORKSPACE_FOLDER`). Folders can be picked from the start of the path by using negative numbers (e.g. `N=-1: WORKSPACE_FOLDER/folder/file.txt -> WORKSPACE_FOLDER`). If the __Item__ is an absolute pattern path, the first folder (`N=-1`) refers to the first folder in the absoulte path, otherwise it corresponds to the workspace folder."), + localize('workbench.editor.label.filename', "`${filename}`: name of the file without the file extension (e.g. `WORKSPACE_FOLDER/folder/file.txt -> file`)."), + localize('workbench.editor.label.extname', "`${extname}`: the file extension (e.g. `WORKSPACE_FOLDER/folder/file.txt -> txt`)."), ].join('\n- '); // intentionally concatenated to not produce a string that is too long for translations - customEditorLabelDescription += '\n\n' + localize('customEditorLabelDescriptionExample', "Example: `\"**/static/**/*.html\": \"${filename} - ${dirname} (${extname})\"` will render a file `root/static/folder/file.html` as `file - folder (html)`."); + customEditorLabelDescription += '\n\n' + localize('customEditorLabelDescriptionExample', "Example: `\"**/static/**/*.html\": \"${filename} - ${dirname} (${extname})\"` will render a file `WORKSPACE_FOLDER/static/folder/file.html` as `file - folder (html)`."); return customEditorLabelDescription; })(), From ef0c9383398ce9f8fb65234a171e44b2ad384b84 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Tue, 7 May 2024 17:23:20 +0200 Subject: [PATCH 019/357] Fixes #208084 (#212176) --- .../multiDiffEditorWidgetImpl.ts | 52 ++++++++++++------- .../browser/widget/multiDiffEditor/style.css | 27 ++++++++++ 2 files changed, 59 insertions(+), 20 deletions(-) diff --git a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts index dab3bff9092..82343308057 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts +++ b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts @@ -30,9 +30,10 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { DiffEditorItemTemplate, TemplateData } from './diffEditorItemTemplate'; import { DocumentDiffItemViewModel, MultiDiffEditorViewModel } from './multiDiffEditorViewModel'; import { ObjectPool } from './objectPool'; +import { localize } from 'vs/nls'; export class MultiDiffEditorWidgetImpl extends Disposable { - private readonly _elements = h('div.monaco-component.multiDiffEditor', [ + private readonly _scrollableElements = h('div.scrollContent', [ h('div@content', { style: { overflow: 'hidden', @@ -42,31 +43,36 @@ export class MultiDiffEditorWidgetImpl extends Disposable { }), ]); - private readonly _sizeObserver = this._register(new ObservableElementSizeObserver(this._element, undefined)); - - private readonly _objectPool = this._register(new ObjectPool((data) => { - const template = this._instantiationService.createInstance( - DiffEditorItemTemplate, - this._elements.content, - this._elements.overflowWidgetsDomNode, - this._workbenchUIElementFactory - ); - template.setData(data); - return template; - })); - private readonly _scrollable = this._register(new Scrollable({ forceIntegerValues: false, scheduleAtNextAnimationFrame: (cb) => scheduleAtNextAnimationFrame(getWindow(this._element), cb), smoothScrollDuration: 100, })); - private readonly _scrollableElement = this._register(new SmoothScrollableElement(this._elements.root, { + private readonly _scrollableElement = this._register(new SmoothScrollableElement(this._scrollableElements.root, { vertical: ScrollbarVisibility.Auto, horizontal: ScrollbarVisibility.Auto, useShadows: false, }, this._scrollable)); + private readonly _elements = h('div.monaco-component.multiDiffEditor', {}, [ + h('div', {}, [this._scrollableElement.getDomNode()]), + h('div.placeholder@placeholder', {}, [h('div', [localize('noChangedFiles', 'No Changed Files') as any])]), + ]); + + private readonly _sizeObserver = this._register(new ObservableElementSizeObserver(this._element, undefined)); + + private readonly _objectPool = this._register(new ObjectPool((data) => { + const template = this._instantiationService.createInstance( + DiffEditorItemTemplate, + this._scrollableElements.content, + this._scrollableElements.overflowWidgetsDomNode, + this._workbenchUIElementFactory + ); + template.setData(data); + return template; + })); + public readonly scrollTop = observableFromEvent(this._scrollableElement.onScroll, () => /** @description scrollTop */ this._scrollableElement.getScrollPosition().scrollTop); public readonly scrollLeft = observableFromEvent(this._scrollableElement.onScroll, () => /** @description scrollLeft */ this._scrollableElement.getScrollPosition().scrollLeft); @@ -150,14 +156,20 @@ export class MultiDiffEditorWidgetImpl extends Disposable { this._sizeObserver.observe(dimension); })); - this._elements.content.style.position = 'relative'; + this._register(autorun((reader) => { + /** @description Update widget dimension */ + const items = this._viewItems.read(reader); + this._elements.placeholder.classList.toggle('visible', items.length === 0); + })); + + this._scrollableElements.content.style.position = 'relative'; this._register(autorun((reader) => { /** @description Update scroll dimensions */ const height = this._sizeObserver.height.read(reader); - this._elements.root.style.height = `${height}px`; + this._scrollableElements.root.style.height = `${height}px`; const totalHeight = this._totalHeight.read(reader); - this._elements.content.style.height = `${totalHeight}px`; + this._scrollableElements.content.style.height = `${totalHeight}px`; const width = this._sizeObserver.width.read(reader); @@ -177,7 +189,7 @@ export class MultiDiffEditorWidgetImpl extends Disposable { }); })); - _element.replaceChildren(this._scrollableElement.getDomNode()); + _element.replaceChildren(this._elements.root); this._register(toDisposable(() => { _element.replaceChildren(); })); @@ -299,7 +311,7 @@ export class MultiDiffEditorWidgetImpl extends Disposable { itemContentHeightSumBefore += itemContentHeight + this._spaceBetweenPx; } - this._elements.content.style.transform = `translateY(${-(scrollTop + contentScrollOffsetToScrollOffset)}px)`; + this._scrollableElements.content.style.transform = `translateY(${-(scrollTop + contentScrollOffsetToScrollOffset)}px)`; } } diff --git a/src/vs/editor/browser/widget/multiDiffEditor/style.css b/src/vs/editor/browser/widget/multiDiffEditor/style.css index 767de682b59..fc9c877bf78 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/style.css +++ b/src/vs/editor/browser/widget/multiDiffEditor/style.css @@ -5,8 +5,35 @@ .monaco-component.multiDiffEditor { background: var(--vscode-multiDiffEditor-background); + + position: relative; + + height: 100%; + width: 100%; + overflow-y: hidden; + > div { + position: absolute; + top: 0px; + left: 0px; + + height: 100%; + width: 100%; + + &.placeholder { + visibility: hidden; + + &.visible { + visibility: visible; + } + + display: grid; + place-items: center; + place-content: center; + } + } + .active { --vscode-multiDiffEditor-border: var(--vscode-focusBorder); } From e2773d08ff996aa764fd58e1d0ea4381f59bcfb7 Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Tue, 7 May 2024 16:59:22 +0200 Subject: [PATCH 020/357] Update distro hash --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e862cfcb31e..e9f2ac5579c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.90.0", - "distro": "739fa8168a41653af56b597dae430dc56601dcaf", + "distro": "600b5db066f5f4807121e5a3da0fa4bb7ed0eb1f", "author": { "name": "Microsoft Corporation" }, From 3455bd54fea4f6536be049e68dfcc3239e290a4b Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 7 May 2024 09:45:43 -0700 Subject: [PATCH 021/357] testing: finalize TestRunRequest.preserveFocus API (#212177) Closes #209491 --- .../workbench/api/common/extHost.api.impl.ts | 1 - src/vs/workbench/api/common/extHostTesting.ts | 4 +-- src/vs/workbench/api/common/extHostTypes.ts | 2 +- .../api/test/browser/extHostTesting.test.ts | 4 ++- .../common/extensionsApiProposals.ts | 1 - src/vscode-dts/vscode.d.ts | 11 +++++++- .../vscode.proposed.testPreserveFocus.d.ts | 28 ------------------- 7 files changed, 16 insertions(+), 35 deletions(-) delete mode 100644 src/vscode-dts/vscode.proposed.testPreserveFocus.d.ts diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 498573b7f1c..297dacd2e4c 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1679,7 +1679,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I LinkedEditingRanges: extHostTypes.LinkedEditingRanges, TestResultState: extHostTypes.TestResultState, TestRunRequest: extHostTypes.TestRunRequest, - TestRunRequest2: extHostTypes.TestRunRequest, TestMessage: extHostTypes.TestMessage, TestTag: extHostTypes.TestTag, TestRunProfileKind: extHostTypes.TestRunProfileKind, diff --git a/src/vs/workbench/api/common/extHostTesting.ts b/src/vs/workbench/api/common/extHostTesting.ts index 27d76030c61..44a28328eab 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/src/vs/workbench/api/common/extHostTesting.ts @@ -210,7 +210,7 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { } await this.proxy.$runTests({ - preserveFocus: (req as vscode.TestRunRequest2).preserveFocus ?? true, + preserveFocus: req.preserveFocus ?? true, targets: [{ testIds: req.include?.map(t => TestId.fromExtHostTestItem(t, controller.collection.root.id).toString()) ?? [controller.collection.root.id], profileGroup: profileGroupToBitset[profile.kind], @@ -746,7 +746,7 @@ export class TestRunCoordinator { exclude: request.exclude?.map(t => TestId.fromExtHostTestItem(t, collection.root.id).toString()) ?? [], id: dto.id, include: request.include?.map(t => TestId.fromExtHostTestItem(t, collection.root.id).toString()) ?? [collection.root.id], - preserveFocus: (request as vscode.TestRunRequest2).preserveFocus ?? true, + preserveFocus: request.preserveFocus ?? true, persist }); diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index c8ba6234f01..220035f98f0 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -4023,7 +4023,7 @@ export enum TestRunProfileKind { } @es5ClassCompat -export class TestRunRequest implements vscode.TestRunRequest2 { +export class TestRunRequest implements vscode.TestRunRequest { constructor( public readonly include: vscode.TestItem[] | undefined = undefined, public readonly exclude: vscode.TestItem[] | undefined = undefined, diff --git a/src/vs/workbench/api/test/browser/extHostTesting.test.ts b/src/vs/workbench/api/test/browser/extHostTesting.test.ts index adcd787f160..0a4723049b6 100644 --- a/src/vs/workbench/api/test/browser/extHostTesting.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTesting.test.ts @@ -658,6 +658,7 @@ suite('ExtHost Testing', () => { include: undefined, exclude: [single.root.children.get('id-b')!], profile: configuration, + preserveFocus: false, }; dto = TestRunDto.fromInternal({ @@ -752,7 +753,7 @@ suite('ExtHost Testing', () => { exclude: [new TestId(['ctrlId', 'id-b']).toString()], persist: false, continuous: false, - preserveFocus: true, + preserveFocus: false, }] ]); @@ -867,6 +868,7 @@ suite('ExtHost Testing', () => { profile: configuration, include: [single.root.children.get('id-a')!], exclude: [single.root.children.get('id-a')!.children.get('id-aa')!], + preserveFocus: false, }, 'hello world', false); task.passed(single.root.children.get('id-a')!.children.get('id-aa')!); diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index 0cbeb156a4c..2ee7867c7c5 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -117,7 +117,6 @@ export const allApiProposals = Object.freeze({ terminalSelection: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalSelection.d.ts', terminalShellIntegration: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalShellIntegration.d.ts', testObserver: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testObserver.d.ts', - testPreserveFocus: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testPreserveFocus.d.ts', textSearchProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.textSearchProvider.d.ts', timeline: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.timeline.d.ts', tokenInformation: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tokenInformation.d.ts', diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index 5e46124e664..41946ad4c0d 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -17400,13 +17400,22 @@ declare module 'vscode' { */ readonly continuous?: boolean; + /** + * Controls how test Test Results view is focused. If true, the editor + * will keep the maintain the user's focus. If false, the editor will + * prefer to move focus into the Test Results view, although + * this may be configured by users. + */ + readonly preserveFocus: boolean; + /** * @param include Array of specific tests to run, or undefined to run all tests * @param exclude An array of tests to exclude from the run. * @param profile The run profile used for this request. * @param continuous Whether to run tests continuously as source changes. + * @param preserveFocus Whether to preserve the user's focus when the run is started */ - constructor(include?: readonly TestItem[], exclude?: readonly TestItem[], profile?: TestRunProfile, continuous?: boolean); + constructor(include?: readonly TestItem[], exclude?: readonly TestItem[], profile?: TestRunProfile, continuous?: boolean, preserveFocus?: boolean); } /** diff --git a/src/vscode-dts/vscode.proposed.testPreserveFocus.d.ts b/src/vscode-dts/vscode.proposed.testPreserveFocus.d.ts deleted file mode 100644 index 1404b4de096..00000000000 --- a/src/vscode-dts/vscode.proposed.testPreserveFocus.d.ts +++ /dev/null @@ -1,28 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -declare module 'vscode' { - - // See https://github.com/microsoft/vscode/issues/209491 - - export class TestRunRequest2 extends TestRunRequest { - /** - * Controls how test Test Results view is focused. If true, the editor - * will keep the maintain the user's focus. If false, the editor will - * prefer to move focus into the Test Results view, although - * this may be configured by users. - */ - readonly preserveFocus: boolean; - - /** - * @param include Array of specific tests to run, or undefined to run all tests - * @param exclude An array of tests to exclude from the run. - * @param profile The run profile used for this request. - * @param continuous Whether to run tests continuously as source changes. - * @param preserveFocus Whether to preserve the user's focus when the run is started - */ - constructor(include?: readonly TestItem[], exclude?: readonly TestItem[], profile?: TestRunProfile, continuous?: boolean); - } -} From c17d1835d4649d2a5f9009a93963ff4c1a4ec441 Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 7 May 2024 18:51:03 +0200 Subject: [PATCH 022/357] lm next --- .../vscode.proposed.chatProvider.d.ts | 4 + .../vscode.proposed.languageModels.d.ts | 133 +++++++++++++++++- 2 files changed, 135 insertions(+), 2 deletions(-) diff --git a/src/vscode-dts/vscode.proposed.chatProvider.d.ts b/src/vscode-dts/vscode.proposed.chatProvider.d.ts index f0eb2380bcf..812f4a001a7 100644 --- a/src/vscode-dts/vscode.proposed.chatProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatProvider.d.ts @@ -42,8 +42,12 @@ declare module 'vscode' { * Additionally, the extension can provide a label that will be shown in the UI. */ auth?: true | { label: string }; + + // MAGIC + extension?: string; } + export namespace chat { /** diff --git a/src/vscode-dts/vscode.proposed.languageModels.d.ts b/src/vscode-dts/vscode.proposed.languageModels.d.ts index bc2ba3a6ed9..675e46967b1 100644 --- a/src/vscode-dts/vscode.proposed.languageModels.d.ts +++ b/src/vscode-dts/vscode.proposed.languageModels.d.ts @@ -116,11 +116,17 @@ declare module 'vscode' { readonly name: string; /** - * The version of the language model. + * Opaque version string of the model. This is defined by the extension contributing the language model + * and subject to change while the identifier is stable. */ - // TODO@API drop this for now? readonly version: string; + /** + * Opaque family-name of the language model. Values might be `gpt-3.5-turbe`, `gpt4`, `phi2`, or `llama` + * but they are defined by extensions contributing languages and subject to change. + */ + family: string; + /** * The number of available tokens that can be used when sending requests * to the language model. @@ -129,9 +135,132 @@ declare module 'vscode' { * * @see {@link lm.sendChatRequest} */ + // TODO@API CAPI only defines prompt_token_count which IMO is just input-tokens readonly contextLength: number; } + // --------------------------- + // Language Model Object (V1) + + export interface LanguageModelInformation2 { + /** + * Human-readable name of the language model. + */ + name: string; + /** + * Opaque family-name of the language model. Values might be `gpt-3.5-turbe`, `gpt4`, `phi2`, or `llama` + * but they are defined by extensions contributing languages and subject to change. + */ + family: string; + /** + * Opaque version string of the model. This is defined by the extension contributing the language model + * and subject to change while the identifier is stable. + */ + version: string; + } + + export interface LanguageModel { + + // TODO@API no id-property needed + readonly id: string; + + readonly info: LanguageModelInformation2; + + sendChatRequest(messages: LanguageModelChatMessage[], options?: LanguageModelChatRequestOptions, token?: CancellationToken): Thenable; + + // maybe optional + computeTokenLength(text: string | LanguageModelChatMessage, token?: CancellationToken): Thenable; + } + + export namespace lm { + + // TODO@API cannot enforce unique language model families, e.g how do we tell `openai.gpt4` apart from `copilot.gpt4` + export function getLanguageModel(family: string): LanguageModel[] | undefined; + + export const languageModels2: LanguageModel[]; + } + // --------------------------- + + + // --------------------------- + // Language Model Object (V2) + // (+) can pick by id or family + // (++) makes it harder to hardcode an identifier of a model in source code + + export interface LanguageModelInformation2 { + /** + * Opaque identifier of the language model. + */ + readonly id: string; + /** + * Human-readable name of the language model. + */ + name: string; + /** + * Opaque family-name of the language model. Values might be `gpt-3.5-turbo`, `gpt4`, `phi2`, or `llama` + * but they are defined by extensions contributing languages and subject to change. + */ + family: string; + /** + * Opaque version string of the model. This is defined by the extension contributing the language model + * and subject to change while the identifier is stable. + */ + version: string; + } + + export interface LanguageModel3 { + /** + * Opaque identifier of the language model. + */ + readonly id: string; + vendor?: string; + /** + * Human-readable name of the language model. + */ + name: string; + /** + * Opaque family-name of the language model. Values might be `gpt-3.5-turbo`, `gpt4`, `phi2`, or `llama` + * but they are defined by extensions contributing languages and subject to change. + */ + family: string; + /** + * Opaque version string of the model. This is defined by the extension contributing the language model + * and subject to change while the identifier is stable. + */ + version: string; + + + // TODO@API + // max_prompt_tokens vs output_tokens vs context_size + contextSize: number; + + sendChatRequest2(messages: LanguageModelChatMessage[], options?: LanguageModelChatRequestOptions, token?: CancellationToken): Thenable; + + computeTokenLength(text: string | LanguageModelChatMessage, token?: CancellationToken): Thenable; + } + + export namespace lm { + + // export const languageModels3: readonly LanguageModelInformation2[]; + + // export const onDidChangeLanguageModels3: Event<{ readonly added: readonly LanguageModelInformation2[]; readonly removed: readonly LanguageModelInformation2[] }>; + + // export const onDidChangeLanguageModels3: Event; + + // (++) lazy activation + // (++) give specific LM to some extension + // // variant A + // export function fetchLanguageModel(selector: { id?: string; family?: string; version?: string }): Thenable; + + // // variant B + export function fetchLanguageModel(selector: { vendor: string; family?: string; version?: string; id?: string }): Thenable; + + // export function sendChatRequest2(languageModel: LanguageModelInformation2, messages: LanguageModelChatMessage[], options?: LanguageModelChatRequestOptions, token?: CancellationToken): Thenable; + + // export function computeTokenLength(languageModel: LanguageModelInformation2, text: string | LanguageModelChatMessage, token?: CancellationToken): Thenable; + } + // --------------------------- + /** * An event describing the change in the set of available language models. */ From c4653faeb1dac658945e8792b5858ef95b2da3d7 Mon Sep 17 00:00:00 2001 From: Michael Lively Date: Tue, 7 May 2024 09:57:17 -0700 Subject: [PATCH 023/357] Use new `getDocumentFormattingEditsWithSelectedProvider` for Notebook formatting (#212185) use new `getDocumentFormattingEditsWithSelectedProvider` --- src/vs/editor/contrib/format/browser/format.ts | 17 +++++++++++++++++ .../browser/contrib/format/formatting.ts | 11 +++++------ .../saveParticipants/saveParticipants.ts | 6 +++--- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/vs/editor/contrib/format/browser/format.ts b/src/vs/editor/contrib/format/browser/format.ts index dbf6ede9eae..bb449bb7934 100644 --- a/src/vs/editor/contrib/format/browser/format.ts +++ b/src/vs/editor/contrib/format/browser/format.ts @@ -414,6 +414,23 @@ export async function getDocumentFormattingEditsUntilResult( return undefined; } +export async function getDocumentFormattingEditsWithSelectedProvider( + workerService: IEditorWorkerService, + languageFeaturesService: ILanguageFeaturesService, + editorOrModel: ITextModel | IActiveCodeEditor, + mode: FormattingMode, + token: CancellationToken, +): Promise { + const model = isCodeEditor(editorOrModel) ? editorOrModel.getModel() : editorOrModel; + const provider = getRealAndSyntheticDocumentFormattersOrdered(languageFeaturesService.documentFormattingEditProvider, languageFeaturesService.documentRangeFormattingEditProvider, model); + const selected = await FormattingConflicts.select(provider, model, mode, FormattingKind.File); + if (selected) { + const rawEdits = await Promise.resolve(selected.provideDocumentFormattingEdits(model, model.getOptions(), token)).catch(onUnexpectedExternalError); + return await workerService.computeMoreMinimalEdits(model.uri, rawEdits); + } + return undefined; +} + export function getOnTypeFormattingEdits( workerService: IEditorWorkerService, languageFeaturesService: ILanguageFeaturesService, diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts b/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts index 7cb9a68437a..166de405964 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts @@ -14,7 +14,7 @@ import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { FormattingMode, formatDocumentWithSelectedProvider, getDocumentFormattingEditsUntilResult } from 'vs/editor/contrib/format/browser/format'; +import { FormattingMode, formatDocumentWithSelectedProvider, getDocumentFormattingEditsWithSelectedProvider } from 'vs/editor/contrib/format/browser/format'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -78,11 +78,11 @@ registerAction2(class extends Action2 { const model = ref.object.textEditorModel; - const formatEdits = await getDocumentFormattingEditsUntilResult( + const formatEdits = await getDocumentFormattingEditsWithSelectedProvider( editorWorkerService, languageFeaturesService, model, - model.getOptions(), + FormattingMode.Explicit, CancellationToken.None ); @@ -177,12 +177,11 @@ class FormatOnCellExecutionParticipant implements ICellExecutionParticipant { const model = ref.object.textEditorModel; - // todo: eventually support cancellation. potential leak if cell deleted mid execution - const formatEdits = await getDocumentFormattingEditsUntilResult( + const formatEdits = await getDocumentFormattingEditsWithSelectedProvider( this.editorWorkerService, this.languageFeaturesService, model, - model.getOptions(), + FormattingMode.Silent, CancellationToken.None ); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/saveParticipants/saveParticipants.ts b/src/vs/workbench/contrib/notebook/browser/contrib/saveParticipants/saveParticipants.ts index 31c62508ca9..2c4c71b98e5 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/saveParticipants/saveParticipants.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/saveParticipants/saveParticipants.ts @@ -20,7 +20,7 @@ import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeat import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { ApplyCodeActionReason, applyCodeAction, getCodeActions } from 'vs/editor/contrib/codeAction/browser/codeAction'; import { CodeActionKind, CodeActionTriggerSource } from 'vs/editor/contrib/codeAction/common/types'; -import { getDocumentFormattingEditsUntilResult } from 'vs/editor/contrib/format/browser/format'; +import { FormattingMode, getDocumentFormattingEditsWithSelectedProvider } from 'vs/editor/contrib/format/browser/format'; import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -73,11 +73,11 @@ class FormatOnSaveParticipant implements IStoredFileWorkingCopySaveParticipant { const model = ref.object.textEditorModel; - const formatEdits = await getDocumentFormattingEditsUntilResult( + const formatEdits = await getDocumentFormattingEditsWithSelectedProvider( this.editorWorkerService, this.languageFeaturesService, model, - model.getOptions(), + FormattingMode.Silent, token ); From 6e009e0537f3c58caffd3fc41cc367d8264ebbde Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 7 May 2024 10:30:08 -0700 Subject: [PATCH 024/357] debug: finalize debugFocus API (#212190) Closes #63943 --- .../workbench/api/common/extHost.api.impl.ts | 4 -- .../common/extensionsApiProposals.ts | 1 - src/vscode-dts/vscode.d.ts | 57 ++++++++++++++++ .../vscode.proposed.debugFocus.d.ts | 66 ------------------- 4 files changed, 57 insertions(+), 71 deletions(-) delete mode 100644 src/vscode-dts/vscode.proposed.debugFocus.d.ts diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 297dacd2e4c..d0b5accb676 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1244,9 +1244,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostDebugService.breakpoints; }, get activeStackItem() { - if (!isProposedApiEnabled(extension, 'debugFocus')) { - return undefined; - } return extHostDebugService.activeStackItem; }, registerDebugVisualizationProvider(id, provider) { @@ -1273,7 +1270,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return _asExtensionEvent(extHostDebugService.onDidChangeBreakpoints)(listener, thisArgs, disposables); }, onDidChangeActiveStackItem(listener, thisArg?, disposables?) { - checkProposedApiEnabled(extension, 'debugFocus'); return _asExtensionEvent(extHostDebugService.onDidChangeActiveStackItem)(listener, thisArg, disposables); }, registerDebugConfigurationProvider(debugType: string, provider: vscode.DebugConfigurationProvider, triggerKind?: vscode.DebugConfigurationProviderTriggerKind) { diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index 2ee7867c7c5..ccc37fcf750 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -50,7 +50,6 @@ export const allApiProposals = Object.freeze({ contribViewsWelcome: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribViewsWelcome.d.ts', createFileSystemWatcher: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.createFileSystemWatcher.d.ts', customEditorMove: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.customEditorMove.d.ts', - debugFocus: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.debugFocus.d.ts', debugVisualization: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.debugVisualization.d.ts', defaultChatParticipant: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.defaultChatParticipant.d.ts', diffCommand: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.diffCommand.d.ts', diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index 41946ad4c0d..f30cfa4a4eb 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -16195,6 +16195,50 @@ declare module 'vscode' { Dynamic = 2 } + /** + * Represents a thread in a debug session. + */ + export class DebugThread { + /** + * Debug session for thread. + */ + readonly session: DebugSession; + + /** + * ID of the associated thread in the debug protocol. + */ + readonly threadId: number; + + /** + * @hidden + */ + private constructor(session: DebugSession, threadId: number); + } + + /** + * Represents a stack frame in a debug session. + */ + export class DebugStackFrame { + /** + * Debug session for thread. + */ + readonly session: DebugSession; + + /** + * ID of the associated thread in the debug protocol. + */ + readonly threadId: number; + /** + * ID of the stack frame in the debug protocol. + */ + readonly frameId: number; + + /** + * @hidden + */ + private constructor(session: DebugSession, threadId: number, frameId: number); + } + /** * Namespace for debug functionality. */ @@ -16245,6 +16289,19 @@ declare module 'vscode' { */ export const onDidChangeBreakpoints: Event; + /** + * The currently focused thread or stack frame, or `undefined` if no + * thread or stack is focused. A thread can be focused any time there is + * an active debug session, while a stack frame can only be focused when + * a session is paused and the call stack has been retrieved. + */ + export const activeStackItem: DebugThread | DebugStackFrame | undefined; + + /** + * An event which fires when the {@link debug.activeStackItem} has changed. + */ + export const onDidChangeActiveStackItem: Event; + /** * Register a {@link DebugConfigurationProvider debug configuration provider} for a specific debug type. * The optional {@link DebugConfigurationProviderTriggerKind triggerKind} can be used to specify when the `provideDebugConfigurations` method of the provider is triggered. diff --git a/src/vscode-dts/vscode.proposed.debugFocus.d.ts b/src/vscode-dts/vscode.proposed.debugFocus.d.ts deleted file mode 100644 index 636cb4745f4..00000000000 --- a/src/vscode-dts/vscode.proposed.debugFocus.d.ts +++ /dev/null @@ -1,66 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -declare module 'vscode' { - - // See https://github.com/microsoft/vscode/issues/63943 - - export class DebugThread { - /** - * Create a ThreadFocus - * @param session - * @param threadId - */ - constructor(session: DebugSession, threadId: number); - - /** - * Debug session for thread. - */ - readonly session: DebugSession; - - /** - * ID of the associated thread in the debug protocol. - */ - readonly threadId: number; - } - - export class DebugStackFrame { - /** - * Create a StackFrameFocus - * @param session - * @param threadId - * @param frameId - */ - constructor(session: DebugSession, threadId?: number, frameId?: number); - - /** - * Debug session for thread. - */ - readonly session: DebugSession; - - /** - * Id of the associated thread in the debug protocol. - */ - readonly threadId: number; - /** - * Id of the stack frame in the debug protocol. - */ - readonly frameId: number; - } - - - export namespace debug { - /** - * The currently focused thread or stack frame, or `undefined` if no - * thread or stack is focused. - */ - export const activeStackItem: DebugThread | DebugStackFrame | undefined; - - /** - * An event which fires when the {@link debug.activeStackItem} has changed. - */ - export const onDidChangeActiveStackItem: Event; - } -} From 5f3e7a0ba9964364d94530915698cdf5470f7d07 Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Tue, 7 May 2024 11:50:03 -0700 Subject: [PATCH 025/357] fix: remove some assertions, ref #211878 (#212137) --- .../browser/preferencesRenderers.ts | 24 ++++++++++++------- .../preferences/browser/preferencesWidgets.ts | 6 +++-- .../preferences/browser/settingsTree.ts | 6 ++--- .../preferences/browser/settingsWidgets.ts | 11 +++++---- 4 files changed, 30 insertions(+), 17 deletions(-) diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts b/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts index 078adf7dfa9..1ce23e61678 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts @@ -395,25 +395,31 @@ class EditSettingRenderer extends Disposable { private getActions(setting: IIndexedSetting, jsonSchema: IJSONSchema): IAction[] { if (jsonSchema.type === 'boolean') { - return [{ + return [{ id: 'truthyValue', label: 'true', + tooltip: 'true', enabled: true, - run: () => this.updateSetting(setting.key, true, setting) - }, { + run: () => this.updateSetting(setting.key, true, setting), + class: undefined + }, { id: 'falsyValue', label: 'false', + tooltip: 'false', enabled: true, - run: () => this.updateSetting(setting.key, false, setting) + run: () => this.updateSetting(setting.key, false, setting), + class: undefined }]; } if (jsonSchema.enum) { return jsonSchema.enum.map(value => { - return { + return { id: value, label: JSON.stringify(value), + tooltip: JSON.stringify(value), enabled: true, - run: () => this.updateSetting(setting.key, value, setting) + run: () => this.updateSetting(setting.key, value, setting), + class: undefined }; }); } @@ -423,11 +429,13 @@ class EditSettingRenderer extends Disposable { private getDefaultActions(setting: IIndexedSetting): IAction[] { if (this.isDefaultSettings()) { const settingInOtherModel = this.associatedPreferencesModel.getPreference(setting.key); - return [{ + return [{ id: 'setDefaultValue', label: settingInOtherModel ? nls.localize('replaceDefaultValue', "Replace in Settings") : nls.localize('copyDefaultValue', "Copy to Settings"), + tooltip: settingInOtherModel ? nls.localize('replaceDefaultValue', "Replace in Settings") : nls.localize('copyDefaultValue', "Copy to Settings"), enabled: true, - run: () => this.updateSetting(setting.key, setting.value, setting) + run: () => this.updateSetting(setting.key, setting.value, setting), + class: undefined }]; } return []; diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts index 7649cc3acca..585b89b4014 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts @@ -185,11 +185,13 @@ export class FolderSettingsActionViewItem extends BaseActionViewItem { if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE && workspaceFolders.length > 0) { actions.push(...workspaceFolders.map((folder, index) => { const folderCount = this._folderSettingCounts.get(folder.uri.toString()); - return { + return { id: 'folderSettingsTarget' + index, label: this.labelWithCount(folder.name, folderCount), - checked: this.folder && isEqual(this.folder.uri, folder.uri), + tooltip: this.labelWithCount(folder.name, folderCount), + checked: !!this.folder && isEqual(this.folder.uri, folder.uri), enabled: true, + class: undefined, run: () => this._action.run(folder) }; })); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index 24ecefc8617..80ce14bcaba 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -1711,12 +1711,12 @@ export class SettingEnumRenderer extends AbstractSettingRenderer implements ITre // Use String constructor in case of null or undefined values const stringifiedDefaultValue = escapeInvisibleChars(String(dataElement.defaultValue)); - const displayOptions = settingEnum + const displayOptions: ISelectOptionItem[] = settingEnum .map(String) .map(escapeInvisibleChars) .map((data, index) => { const description = (enumDescriptions[index] && (enumDescriptionsAreMarkdown ? fixSettingLinks(enumDescriptions[index], false) : enumDescriptions[index])); - return { + return { text: enumItemLabels[index] ? enumItemLabels[index] : data, detail: enumItemLabels[index] ? data : '', description, @@ -1728,7 +1728,7 @@ export class SettingEnumRenderer extends AbstractSettingRenderer implements ITre disposables: disposables }, decoratorRight: (((data === stringifiedDefaultValue) || (createdDefault && index === 0)) ? localize('settings.Default', "default") : '') - }; + } satisfies ISelectOptionItem; }); template.selectBox.setOptions(displayOptions); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts index 80736dc0e3b..4c9362f217c 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts @@ -894,32 +894,35 @@ export class ObjectSettingDropdownWidget extends AbstractListSettingWidget this.editSetting(idx) }, - ] as IAction[]; + ]; if (item.removable) { actions.push({ class: ThemeIcon.asClassName(settingsRemoveIcon), enabled: true, id: 'workbench.action.removeListItem', + label: this.getLocalizedStrings().deleteActionTooltip, tooltip: this.getLocalizedStrings().deleteActionTooltip, run: () => this._onDidChangeList.fire({ originalItem: item, item: undefined, targetIndex: idx }) - } as IAction); + }); } else { actions.push({ class: ThemeIcon.asClassName(settingsDiscardIcon), enabled: true, id: 'workbench.action.resetListItem', + label: this.getLocalizedStrings().resetActionTooltip, tooltip: this.getLocalizedStrings().resetActionTooltip, run: () => this._onDidChangeList.fire({ originalItem: item, item: undefined, targetIndex: idx }) - } as IAction); + }); } return actions; From 9aaf087e6bb1fff5d032e3885d577db289f0f24b Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 7 May 2024 12:01:00 -0700 Subject: [PATCH 026/357] debug: maintain position when inline values are on with word wrap, fix flickering (#212192) * debug: avoid inline value flickering during refresh Fixes #210733 * debug: maintain position when inline values are on with word wrap --- .../debug/browser/debugEditorContribution.ts | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts index c13e82fd187..4ab2ba9d777 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts @@ -25,11 +25,12 @@ import { Constants } from 'vs/base/common/uint'; import { URI } from 'vs/base/common/uri'; import { CoreEditingCommands } from 'vs/editor/browser/coreCommands'; import { ICodeEditor, IEditorMouseEvent, IPartialEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; -import { IEditorHoverOptions } from 'vs/editor/common/config/editorOptions'; +import { EditorOption, IEditorHoverOptions } from 'vs/editor/common/config/editorOptions'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { DEFAULT_WORD_REGEXP } from 'vs/editor/common/core/wordHelper'; +import { ScrollType } from 'vs/editor/common/editorCommon'; import { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; import { InlineValue, InlineValueContext } from 'vs/editor/common/languages'; import { IModelDeltaDecoration, ITextModel, InjectedTextCursorStops } from 'vs/editor/common/model'; @@ -641,6 +642,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { return new RunOnceScheduler( () => { this.displayedStore.clear(); + this.oldDecorations.clear(); }, 100 ); @@ -803,9 +805,26 @@ export class DebugEditorContribution implements IDebugEditorContribution { decoration => `${decoration.range.startLineNumber}:${decoration?.options.after?.content}`); } - if (!cts.token.isCancellationRequested) { - this.oldDecorations.set(allDecorations); - this.displayedStore.add(toDisposable(() => this.oldDecorations.clear())); + if (cts.token.isCancellationRequested) { + return; + } + + // If word wrap is on, application of inline decorations may change the scroll position. + // Ensure the cursor maintains its vertical position relative to the viewport when + // we apply decorations. + let preservePosition: { position: Position; top: number } | undefined; + if (this.editor.getOption(EditorOption.wordWrap) !== 'off') { + const position = this.editor.getPosition(); + if (position && this.editor.getVisibleRanges().some(r => r.containsPosition(position))) { + preservePosition = { position, top: this.editor.getTopForPosition(position.lineNumber, position.column) }; + } + } + + this.oldDecorations.set(allDecorations); + + if (preservePosition) { + const top = this.editor.getTopForPosition(preservePosition.position.lineNumber, preservePosition.position.column); + this.editor.setScrollTop(this.editor.getScrollTop() - (preservePosition.top - top), ScrollType.Immediate); } } From aed0775809306c6d817c74a18e638192afc35547 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 7 May 2024 21:33:23 +0200 Subject: [PATCH 027/357] speech - integrate text-to-speech for chat items (#212121) --- src/vs/workbench/api/common/extHostSpeech.ts | 25 +- .../chat/browser/actions/chatActions.ts | 9 + .../chat/browser/actions/chatCopyActions.ts | 10 +- .../actions/media/voiceChatActions.css | 12 +- .../actions/voiceChatActions.ts | 225 +++++++++++++----- .../electron-sandbox/chat.contribution.ts | 5 +- .../browser/speechAccessibilitySignal.ts | 1 + .../contrib/speech/browser/speechService.ts | 4 +- src/vscode-dts/vscode.proposed.speech.d.ts | 12 +- 9 files changed, 217 insertions(+), 86 deletions(-) diff --git a/src/vs/workbench/api/common/extHostSpeech.ts b/src/vs/workbench/api/common/extHostSpeech.ts index abc56cedc08..da563dcdb4d 100644 --- a/src/vs/workbench/api/common/extHostSpeech.ts +++ b/src/vs/workbench/api/common/extHostSpeech.ts @@ -36,7 +36,11 @@ export class ExtHostSpeech implements ExtHostSpeechShape { const cts = new CancellationTokenSource(); this.sessions.set(session, cts); - const speechToTextSession = disposables.add(provider.provideSpeechToTextSession(cts.token, language ? { language } : undefined)); + const speechToTextSession = await provider.provideSpeechToTextSession(cts.token, language ? { language } : undefined); + if (!speechToTextSession) { + return; + } + disposables.add(speechToTextSession.onDidChange(e => { if (cts.token.isCancellationRequested) { return; @@ -64,7 +68,11 @@ export class ExtHostSpeech implements ExtHostSpeechShape { const cts = new CancellationTokenSource(); this.sessions.set(session, cts); - const textToSpeech = disposables.add(provider.provideTextToSpeechSession(cts.token)); + const textToSpeech = await provider.provideTextToSpeechSession(cts.token); + if (!textToSpeech) { + return; + } + this.synthesizers.set(session, textToSpeech); disposables.add(textToSpeech.onDidChange(e => { @@ -79,12 +87,7 @@ export class ExtHostSpeech implements ExtHostSpeechShape { } async $synthesizeSpeech(session: number, text: string): Promise { - const synthesizer = this.synthesizers.get(session); - if (!synthesizer) { - return; - } - - synthesizer.synthesize(text); + this.synthesizers.get(session)?.synthesize(text); } async $cancelTextToSpeechSession(session: number): Promise { @@ -104,7 +107,11 @@ export class ExtHostSpeech implements ExtHostSpeechShape { const cts = new CancellationTokenSource(); this.sessions.set(session, cts); - const keywordRecognitionSession = disposables.add(provider.provideKeywordRecognitionSession(cts.token)); + const keywordRecognitionSession = await provider.provideKeywordRecognitionSession(cts.token); + if (!keywordRecognitionSession) { + return; + } + disposables.add(keywordRecognitionSession.onDidChange(e => { if (cts.token.isCancellationRequested) { return; diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 7545ae0ccdd..03cd71f82e2 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -22,6 +22,7 @@ import { ChatViewPane } from 'vs/workbench/contrib/chat/browser/chatViewPane'; import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; import { CONTEXT_CHAT_INPUT_CURSOR_AT_TOP, CONTEXT_CHAT_LOCATION, CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION, CONTEXT_CHAT_ENABLED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatDetail, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatRequestViewModel, IChatResponseViewModel, isRequestVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { IChatWidgetHistoryService } from 'vs/workbench/contrib/chat/common/chatWidgetHistoryService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; @@ -239,3 +240,11 @@ export function registerChatActions() { } }); } + +export function stringifyItem(item: IChatRequestViewModel | IChatResponseViewModel, includeName = true): string { + if (isRequestVM(item)) { + return (includeName ? `${item.username}: ` : '') + item.messageText; + } else { + return (includeName ? `${item.username}: ` : '') + item.response.asString(); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts index c55739a639a..5504c8e556f 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts @@ -7,7 +7,7 @@ import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { localize2 } from 'vs/nls'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; +import { CHAT_CATEGORY, stringifyItem } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { CONTEXT_RESPONSE_FILTERED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatRequestViewModel, IChatResponseViewModel, isRequestVM, isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; @@ -72,11 +72,3 @@ export function registerChatCopyActions() { } }); } - -function stringifyItem(item: IChatRequestViewModel | IChatResponseViewModel, includeName = true): string { - if (isRequestVM(item)) { - return (includeName ? `${item.username}: ` : '') + item.messageText; - } else { - return (includeName ? `${item.username}: ` : '') + item.response.asString(); - } -} diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/actions/media/voiceChatActions.css b/src/vs/workbench/contrib/chat/electron-sandbox/actions/media/voiceChatActions.css index ba4f2a36513..beae62f5939 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/actions/media/voiceChatActions.css +++ b/src/vs/workbench/contrib/chat/electron-sandbox/actions/media/voiceChatActions.css @@ -4,16 +4,25 @@ *--------------------------------------------------------------------------------------------*/ /* - * Replace with "microphone" icon. + * Replace "loading" with "microphone" icon. */ .monaco-workbench .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before { content: var(--vscode-icon-mic-filled-content); font-family: var(--vscode-icon-mic-filled-font-family); } +/* + * Replace "sync" with "pulse" icon. + */ +.monaco-workbench .interactive-input-part .monaco-action-bar .action-label.codicon-sync.codicon-modifier-spin:not(.disabled)::before { + content: var(--vscode-icon-pulse-content); + font-family: var(--vscode-icon-pulse-font-family); +} + /* * Clear animation styles when reduced motion is enabled. */ +.monaco-workbench.reduce-motion .interactive-input-part .monaco-action-bar .action-label.codicon-sync.codicon-modifier-spin:not(.disabled), .monaco-workbench.reduce-motion .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled) { animation: none; } @@ -21,6 +30,7 @@ /* * Replace with "stop" icon when reduced motion is enabled. */ +.monaco-workbench.reduce-motion .interactive-input-part .monaco-action-bar .action-label.codicon-sync.codicon-modifier-spin:not(.disabled)::before, .monaco-workbench.reduce-motion .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before { content: var(--vscode-icon-debug-stop-content); font-family: var(--vscode-icon-debug-stop-font-family); diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts index 2b81361eeed..999c93c92cd 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import 'vs/css!./media/voiceChatActions'; import { RunOnceScheduler, disposableTimeout } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Codicon } from 'vs/base/common/codicons'; @@ -12,7 +13,6 @@ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ThemeIcon } from 'vs/base/common/themables'; import { assertIsDefined, isNumber } from 'vs/base/common/types'; -import 'vs/css!./media/voiceChatActions'; import { getCodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { localize, localize2 } from 'vs/nls'; @@ -27,25 +27,26 @@ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegis import { ProgressLocation } from 'vs/platform/progress/common/progress'; import { Registry } from 'vs/platform/registry/common/platform'; import { contrastBorder, focusBorder } from 'vs/platform/theme/common/colorRegistry'; -import { spinningLoading } from 'vs/platform/theme/common/iconRegistry'; +import { spinningLoading, syncing } from 'vs/platform/theme/common/iconRegistry'; import { ColorScheme } from 'vs/platform/theme/common/theme'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { ActiveEditorContext } from 'vs/workbench/common/contextkeys'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { ACTIVITY_BAR_BADGE_BACKGROUND } from 'vs/workbench/common/theme'; import { AccessibilityVoiceSettingId, SpeechTimeoutDefault, accessibilityConfigurationNodeBase } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; +import { CHAT_CATEGORY, stringifyItem } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; import { IChatExecuteActionContext } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions'; import { CHAT_VIEW_ID, IChatWidget, IChatWidgetService, IQuickChatService, showChatView } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatAgentLocation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { CONTEXT_CHAT_REQUEST_IN_PROGRESS, CONTEXT_IN_CHAT_INPUT, CONTEXT_CHAT_ENABLED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { CONTEXT_CHAT_REQUEST_IN_PROGRESS, CONTEXT_IN_CHAT_INPUT, CONTEXT_CHAT_ENABLED, CONTEXT_RESPONSE_FILTERED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatService, KEYWORD_ACTIVIATION_SETTING_ID } from 'vs/workbench/contrib/chat/common/chatService'; +import { isRequestVM, isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { IVoiceChatService } from 'vs/workbench/contrib/chat/common/voiceChat'; import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; import { InlineChatController } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; -import { HasSpeechProvider, ISpeechService, KeywordRecognitionStatus, SpeechToTextStatus } from 'vs/workbench/contrib/speech/common/speechService'; +import { HasSpeechProvider, ISpeechService, KeywordRecognitionStatus, SpeechToTextStatus, SpeechToTextInProgress, TextToSpeechInProgress } from 'vs/workbench/contrib/speech/common/speechService'; import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalChatContextKeys, TerminalChatController } from 'vs/workbench/contrib/terminal/browser/terminalContribExports'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -54,6 +55,8 @@ import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/bro import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/statusbar/browser/statusbar'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; +//#region Speech to Text + const CONTEXT_VOICE_CHAT_GETTING_READY = new RawContextKey('voiceChatGettingReady', false, { type: 'boolean', description: localize('voiceChatGettingReady', "True when getting ready for receiving voice input from the microphone for voice chat.") }); const CONTEXT_VOICE_CHAT_IN_PROGRESS = new RawContextKey('voiceChatInProgress', false, { type: 'boolean', description: localize('voiceChatInProgress', "True when voice recording from microphone is in progress for voice chat.") }); @@ -614,12 +617,12 @@ export class StartVoiceChatAction extends Action2 { precondition: ContextKeyExpr.and(CanVoiceChat, CONTEXT_VOICE_CHAT_GETTING_READY.negate(), CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate(), CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST.negate(), TerminalChatContextKeys.requestActive.negate()), menu: [{ id: MenuId.ChatExecute, - when: ContextKeyExpr.and(HasSpeechProvider, CONTEXT_VOICE_CHAT_IN_VIEW_IN_PROGRESS.negate(), CONTEXT_QUICK_VOICE_CHAT_IN_PROGRESS.negate(), CONTEXT_VOICE_CHAT_IN_EDITOR_IN_PROGRESS.negate()), + when: ContextKeyExpr.and(HasSpeechProvider, CONTEXT_VOICE_CHAT_IN_VIEW_IN_PROGRESS.negate(), CONTEXT_QUICK_VOICE_CHAT_IN_PROGRESS.negate(), CONTEXT_VOICE_CHAT_IN_EDITOR_IN_PROGRESS.negate(), TextToSpeechInProgress.negate()), group: 'navigation', order: -1 }, { id: MenuId.for('terminalChatInput'), - when: ContextKeyExpr.and(HasSpeechProvider, CONTEXT_TERMINAL_VOICE_CHAT_IN_PROGRESS.negate()), + when: ContextKeyExpr.and(HasSpeechProvider, CONTEXT_TERMINAL_VOICE_CHAT_IN_PROGRESS.negate(), TextToSpeechInProgress.negate()), group: 'navigation', order: -1 }] @@ -787,65 +790,105 @@ export class StopListeningAndSubmitAction extends Action2 { } } -registerThemingParticipant((theme, collector) => { - let activeRecordingColor: Color | undefined; - let activeRecordingDimmedColor: Color | undefined; - if (theme.type === ColorScheme.LIGHT || theme.type === ColorScheme.DARK) { - activeRecordingColor = theme.getColor(ACTIVITY_BAR_BADGE_BACKGROUND) ?? theme.getColor(focusBorder); - activeRecordingDimmedColor = activeRecordingColor?.transparent(0.38); - } else { - activeRecordingColor = theme.getColor(contrastBorder); - activeRecordingDimmedColor = theme.getColor(contrastBorder); +//#endregion + +//#region Text to Speech + +class TextToSpeechSessions { + + private static instance: TextToSpeechSessions | undefined = undefined; + static getInstance(instantiationService: IInstantiationService): TextToSpeechSessions { + if (!TextToSpeechSessions.instance) { + TextToSpeechSessions.instance = instantiationService.createInstance(TextToSpeechSessions); + } + + return TextToSpeechSessions.instance; } - // Show a "microphone" icon when recording is in progress that glows via outline. - collector.addRule(` - .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled) { - color: ${activeRecordingColor}; - outline: 1px solid ${activeRecordingColor}; - outline-offset: -1px; - animation: pulseAnimation 1s infinite; - border-radius: 50%; - } + private activeSession: CancellationTokenSource | undefined = undefined; - .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before { - position: absolute; - outline: 1px solid ${activeRecordingColor}; - outline-offset: 2px; - border-radius: 50%; - width: 16px; - height: 16px; - } + constructor( + @ISpeechService private readonly speechService: ISpeechService + ) { } - .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::after { - outline: 2px solid ${activeRecordingColor}; - outline-offset: -1px; - animation: pulseAnimation 1500ms cubic-bezier(0.75, 0, 0.25, 1) infinite; - } + async start(text: string): Promise { + this.stop(); - .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before { - position: absolute; - outline: 1px solid ${activeRecordingColor}; - outline-offset: 2px; - border-radius: 50%; - width: 16px; - height: 16px; - } + const activeSession = this.activeSession = new CancellationTokenSource(); - @keyframes pulseAnimation { - 0% { - outline-width: 2px; - } - 62% { - outline-width: 5px; - outline-color: ${activeRecordingDimmedColor}; - } - 100% { - outline-width: 2px; + const session = await this.speechService.createTextToSpeechSession(activeSession.token, 'chat'); + session.synthesize(text); + } + + stop(): void { + this.activeSession?.dispose(true); + this.activeSession = undefined; + } +} + +export class ReadChatItemAloud extends Action2 { + constructor() { + super({ + id: 'workbench.action.chat.readChatItemAloud', + title: localize2('workbench.action.chat.readChatItemAloud', "Read Aloud"), + f1: false, + category: CHAT_CATEGORY, + precondition: ContextKeyExpr.and(CanVoiceChat, SpeechToTextInProgress.toNegated()), + menu: { + id: MenuId.ChatContext, + when: ContextKeyExpr.and(CanVoiceChat, CONTEXT_RESPONSE_FILTERED.toNegated()), + group: 'textToSpeech' } + }); + } + + run(accessor: ServicesAccessor, ...args: any[]) { + const item = args[0]; + if (!isRequestVM(item) && !isResponseVM(item)) { + return; } - `); -}); + + TextToSpeechSessions.getInstance(accessor.get(IInstantiationService)).start(stringifyItem(item, false)); + } +} + +export class StopReadAloud extends Action2 { + + static readonly ID = 'workbench.action.speech.stopReadAloud'; + + constructor() { + super({ + id: StopReadAloud.ID, + icon: syncing, + title: localize2('workbench.action.speech.stopReadAloud', "Stop Reading Aloud"), + f1: true, + category: CHAT_CATEGORY, + precondition: TextToSpeechInProgress, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib + 100, + primary: KeyCode.Escape + }, + menu: [{ + id: MenuId.ChatContext, + when: ContextKeyExpr.and(CanVoiceChat, TextToSpeechInProgress), + group: 'textToSpeech' + }, { + id: MenuId.ChatExecute, + when: ContextKeyExpr.and(CanVoiceChat, TextToSpeechInProgress), + group: 'navigation', + order: -1 + }] + }); + } + + async run(accessor: ServicesAccessor, ...args: any[]) { + TextToSpeechSessions.getInstance(accessor.get(IInstantiationService)).stop(); + } +} + +//#endregion + +//#region Keyword Recognition function supportsKeywordActivation(configurationService: IConfigurationService, speechService: ISpeechService, chatAgentService: IChatAgentService): boolean { if (!speechService.hasSpeechProvider || !chatAgentService.getDefaultAgent(ChatAgentLocation.Panel)) { @@ -1086,3 +1129,69 @@ class KeywordActivationStatusEntry extends Disposable { this.entry.value?.update(this.getStatusEntryProperties()); } } + +//#endregion + +registerThemingParticipant((theme, collector) => { + let activeRecordingColor: Color | undefined; + let activeRecordingDimmedColor: Color | undefined; + if (theme.type === ColorScheme.LIGHT || theme.type === ColorScheme.DARK) { + activeRecordingColor = theme.getColor(ACTIVITY_BAR_BADGE_BACKGROUND) ?? theme.getColor(focusBorder); + activeRecordingDimmedColor = activeRecordingColor?.transparent(0.38); + } else { + activeRecordingColor = theme.getColor(contrastBorder); + activeRecordingDimmedColor = theme.getColor(contrastBorder); + } + + // Show a "microphone" or "pulse" icon when speech-to-text or text-to-speech is in progress that glows via outline. + collector.addRule(` + .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-sync.codicon-modifier-spin:not(.disabled), + .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled) { + color: ${activeRecordingColor}; + outline: 1px solid ${activeRecordingColor}; + outline-offset: -1px; + animation: pulseAnimation 1s infinite; + border-radius: 50%; + } + + .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-sync.codicon-modifier-spin:not(.disabled)::before, + .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before { + position: absolute; + outline: 1px solid ${activeRecordingColor}; + outline-offset: 2px; + border-radius: 50%; + width: 16px; + height: 16px; + } + + .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-sync.codicon-modifier-spin:not(.disabled)::after, + .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::after { + outline: 2px solid ${activeRecordingColor}; + outline-offset: -1px; + animation: pulseAnimation 1500ms cubic-bezier(0.75, 0, 0.25, 1) infinite; + } + + .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-sync.codicon-modifier-spin:not(.disabled)::before, + .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before { + position: absolute; + outline: 1px solid ${activeRecordingColor}; + outline-offset: 2px; + border-radius: 50%; + width: 16px; + height: 16px; + } + + @keyframes pulseAnimation { + 0% { + outline-width: 2px; + } + 62% { + outline-width: 5px; + outline-color: ${activeRecordingDimmedColor}; + } + 100% { + outline-width: 2px; + } + } + `); +}); diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts b/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts index 186a6a715c1..5ef9c6a6c67 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { InlineVoiceChatAction, QuickVoiceChatAction, StartVoiceChatAction, StopListeningInQuickChatAction, StopListeningInChatEditorAction, StopListeningInChatViewAction, VoiceChatInChatViewAction, StopListeningAction, StopListeningAndSubmitAction, KeywordActivationContribution, InstallVoiceChatAction, StopListeningInTerminalChatAction, HoldToVoiceChatInChatViewAction } from 'vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions'; +import { InlineVoiceChatAction, QuickVoiceChatAction, StartVoiceChatAction, StopListeningInQuickChatAction, StopListeningInChatEditorAction, StopListeningInChatViewAction, VoiceChatInChatViewAction, StopListeningAction, StopListeningAndSubmitAction, KeywordActivationContribution, InstallVoiceChatAction, StopListeningInTerminalChatAction, HoldToVoiceChatInChatViewAction, ReadChatItemAloud, StopReadAloud } from 'vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions'; import { registerAction2 } from 'vs/platform/actions/common/actions'; import { WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; @@ -23,4 +23,7 @@ registerAction2(StopListeningInChatEditorAction); registerAction2(StopListeningInQuickChatAction); registerAction2(StopListeningInTerminalChatAction); +registerAction2(ReadChatItemAloud); +registerAction2(StopReadAloud); + registerWorkbenchContribution2(KeywordActivationContribution.ID, KeywordActivationContribution, WorkbenchPhase.AfterRestored); diff --git a/src/vs/workbench/contrib/speech/browser/speechAccessibilitySignal.ts b/src/vs/workbench/contrib/speech/browser/speechAccessibilitySignal.ts index 5df6a700592..7bc183df9ed 100644 --- a/src/vs/workbench/contrib/speech/browser/speechAccessibilitySignal.ts +++ b/src/vs/workbench/contrib/speech/browser/speechAccessibilitySignal.ts @@ -17,6 +17,7 @@ export class SpeechAccessibilitySignalContribution extends Disposable implements @ISpeechService private readonly _speechService: ISpeechService, ) { super(); + this._register(this._speechService.onDidStartSpeechToTextSession(() => this._accessibilitySignalService.playSignal(AccessibilitySignal.voiceRecordingStarted))); this._register(this._speechService.onDidEndSpeechToTextSession(() => this._accessibilitySignalService.playSignal(AccessibilitySignal.voiceRecordingStopped))); } diff --git a/src/vs/workbench/contrib/speech/browser/speechService.ts b/src/vs/workbench/contrib/speech/browser/speechService.ts index 25d5c0ce951..7dc6c571132 100644 --- a/src/vs/workbench/contrib/speech/browser/speechService.ts +++ b/src/vs/workbench/contrib/speech/browser/speechService.ts @@ -126,7 +126,7 @@ export class SpeechService extends Disposable implements ISpeechService { this._onDidChangeHasSpeechProvider.fire(); } - //#region Transcription + //#region Speech to Text private readonly _onDidStartSpeechToTextSession = this._register(new Emitter()); readonly onDidStartSpeechToTextSession = this._onDidStartSpeechToTextSession.event; @@ -240,7 +240,7 @@ export class SpeechService extends Disposable implements ISpeechService { //#endregion - //#region Synthesizer + //#region Text to Speech private readonly _onDidStartTextToSpeechSession = this._register(new Emitter()); readonly onDidStartTextToSpeechSession = this._onDidStartTextToSpeechSession.event; diff --git a/src/vscode-dts/vscode.proposed.speech.d.ts b/src/vscode-dts/vscode.proposed.speech.d.ts index 4e0ad0031ce..b450ed18bf2 100644 --- a/src/vscode-dts/vscode.proposed.speech.d.ts +++ b/src/vscode-dts/vscode.proposed.speech.d.ts @@ -22,7 +22,7 @@ declare module 'vscode' { readonly text?: string; } - export interface SpeechToTextSession extends Disposable { + export interface SpeechToTextSession { readonly onDidChange: Event; } @@ -37,7 +37,7 @@ declare module 'vscode' { readonly text?: string; } - export interface TextToSpeechSession extends Disposable { + export interface TextToSpeechSession { readonly onDidChange: Event; synthesize(text: string): void; @@ -53,14 +53,14 @@ declare module 'vscode' { readonly text?: string; } - export interface KeywordRecognitionSession extends Disposable { + export interface KeywordRecognitionSession { readonly onDidChange: Event; } export interface SpeechProvider { - provideSpeechToTextSession(token: CancellationToken, options?: SpeechToTextOptions): SpeechToTextSession; - provideTextToSpeechSession(token: CancellationToken): TextToSpeechSession; - provideKeywordRecognitionSession(token: CancellationToken): KeywordRecognitionSession; + provideSpeechToTextSession(token: CancellationToken, options?: SpeechToTextOptions): ProviderResult; + provideTextToSpeechSession(token: CancellationToken): ProviderResult; + provideKeywordRecognitionSession(token: CancellationToken): ProviderResult; } export namespace speech { From 071b61c18454b29357662e549d1ee3c02fe6a81f Mon Sep 17 00:00:00 2001 From: Mahmoud Salah Date: Tue, 7 May 2024 21:35:44 +0200 Subject: [PATCH 028/357] =?UTF-8?q?Fire=20onDidRegisterAllSupported=20exec?= =?UTF-8?q?utions=20if=20any=20execution=20type=20is=20re=E2=80=A6=20(#212?= =?UTF-8?q?163)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index 1a02a115033..0e9b7879ba0 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -388,7 +388,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer // update tasks so an incomplete list isn't returned when getWorkspaceTasks is called this._workspaceTasksPromise = undefined; this._onDidRegisterSupportedExecutions.fire(); - if (custom && shell && process) { + if (Platform.isWeb || (custom && shell && process)) { this._onDidRegisterAllSupportedExecutions.fire(); } } From 40156c5d063d1dfe0d5cca18c148d8a31413e801 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 7 May 2024 19:44:49 +0000 Subject: [PATCH 029/357] SCM - delete old code that is not used (#212199) --- src/vs/workbench/contrib/scm/common/scmService.ts | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/vs/workbench/contrib/scm/common/scmService.ts b/src/vs/workbench/contrib/scm/common/scmService.ts index 7e85cdaa4c1..762dc9ed1b6 100644 --- a/src/vs/workbench/contrib/scm/common/scmService.ts +++ b/src/vs/workbench/contrib/scm/common/scmService.ts @@ -5,7 +5,7 @@ import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; -import { ISCMService, ISCMProvider, ISCMInput, ISCMRepository, IInputValidator, ISCMInputChangeEvent, SCMInputChangeReason, InputValidationType, IInputValidation, ISCMActionButtonDescriptor } from './scm'; +import { ISCMService, ISCMProvider, ISCMInput, ISCMRepository, IInputValidator, ISCMInputChangeEvent, SCMInputChangeReason, InputValidationType, IInputValidation } from './scm'; import { ILogService } from 'vs/platform/log/common/log'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; @@ -69,19 +69,6 @@ class SCMInput implements ISCMInput { private readonly _onDidChangeVisibility = new Emitter(); readonly onDidChangeVisibility: Event = this._onDidChangeVisibility.event; - private _actionButton: ISCMActionButtonDescriptor | undefined; - get actionButton(): ISCMActionButtonDescriptor | undefined { - return this._actionButton; - } - - set actionButton(actionButton: ISCMActionButtonDescriptor) { - this._actionButton = actionButton; - this._onDidChangeActionButton.fire(); - } - - private readonly _onDidChangeActionButton = new Emitter(); - readonly onDidChangeActionButton: Event = this._onDidChangeActionButton.event; - setFocus(): void { this._onDidChangeFocus.fire(); } From 8b86b2f26a656060086929441db4bdade0d4ac03 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 7 May 2024 13:04:19 -0700 Subject: [PATCH 030/357] task part of #211878 (#211973) --- .../contrib/tasks/common/taskConfiguration.ts | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts b/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts index 511383f85c1..0de3b0a3839 100644 --- a/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts +++ b/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts @@ -1003,8 +1003,7 @@ namespace CommandConfiguration { runtime = Tasks.RuntimeType.fromString(config.type); } } - const isShellConfiguration = ShellConfiguration.is(config.isShellCommand); - if (Types.isBoolean(config.isShellCommand) || isShellConfiguration) { + if (Types.isBoolean(config.isShellCommand) || ShellConfiguration.is(config.isShellCommand)) { runtime = Tasks.RuntimeType.Shell; } else if (config.isShellCommand !== undefined) { runtime = !!config.isShellCommand ? Tasks.RuntimeType.Shell : Tasks.RuntimeType.Process; @@ -1034,8 +1033,8 @@ namespace CommandConfiguration { } if (config.options !== undefined) { result.options = CommandOptions.from(config.options, context); - if (result.options && result.options.shell === undefined && isShellConfiguration) { - result.options.shell = ShellConfiguration.from(config.isShellCommand as IShellConfiguration, context); + if (result.options && result.options.shell === undefined && ShellConfiguration.is(config.isShellCommand)) { + result.options.shell = ShellConfiguration.from(config.isShellCommand, context); if (context.engine !== Tasks.ExecutionEngine.Terminal) { context.taskLoadIssues.push(nls.localize('ConfigurationParser.noShell', 'Warning: shell configuration is only supported when executing tasks in the terminal.')); } @@ -1247,11 +1246,6 @@ export namespace ProblemMatcherConverter { } } -const partialSource: Partial = { - label: 'Workspace', - config: undefined -}; - export namespace GroupKind { export function from(this: void, external: string | IGroupKind | undefined): Tasks.TaskGroup | undefined { if (external === undefined) { @@ -1399,6 +1393,7 @@ namespace ConfigurationProperties { return _isEmpty(value, properties); } } +const label = 'Workspace'; namespace ConfiguringTask { @@ -1470,15 +1465,15 @@ namespace ConfiguringTask { let taskSource: Tasks.FileBasedTaskSource; switch (source) { case TaskConfigSource.User: { - taskSource = Object.assign({} as Tasks.IUserTaskSource, partialSource, { kind: Tasks.TaskSourceKind.User, config: configElement }); + taskSource = { kind: Tasks.TaskSourceKind.User, config: configElement, label }; break; } case TaskConfigSource.WorkspaceFile: { - taskSource = Object.assign({} as Tasks.WorkspaceFileTaskSource, partialSource, { kind: Tasks.TaskSourceKind.WorkspaceFile, config: configElement }); + taskSource = { kind: Tasks.TaskSourceKind.WorkspaceFile, config: configElement, label }; break; } default: { - taskSource = Object.assign({} as Tasks.IWorkspaceTaskSource, partialSource, { kind: Tasks.TaskSourceKind.Workspace, config: configElement }); + taskSource = { kind: Tasks.TaskSourceKind.Workspace, config: configElement, label }; break; } } @@ -1543,15 +1538,15 @@ namespace CustomTask { let taskSource: Tasks.FileBasedTaskSource; switch (source) { case TaskConfigSource.User: { - taskSource = Object.assign({} as Tasks.IUserTaskSource, partialSource, { kind: Tasks.TaskSourceKind.User, config: { index, element: external, file: '.vscode/tasks.json', workspaceFolder: context.workspaceFolder } }); + taskSource = { kind: Tasks.TaskSourceKind.User, config: { index, element: external, file: '.vscode/tasks.json', workspaceFolder: context.workspaceFolder }, label }; break; } case TaskConfigSource.WorkspaceFile: { - taskSource = Object.assign({} as Tasks.WorkspaceFileTaskSource, partialSource, { kind: Tasks.TaskSourceKind.WorkspaceFile, config: { index, element: external, file: '.vscode/tasks.json', workspaceFolder: context.workspaceFolder, workspace: context.workspace } }); + taskSource = { kind: Tasks.TaskSourceKind.WorkspaceFile, config: { index, element: external, file: '.vscode/tasks.json', workspaceFolder: context.workspaceFolder, workspace: context.workspace }, label }; break; } default: { - taskSource = Object.assign({} as Tasks.IWorkspaceTaskSource, partialSource, { kind: Tasks.TaskSourceKind.Workspace, config: { index, element: external, file: '.vscode/tasks.json', workspaceFolder: context.workspaceFolder } }); + taskSource = { kind: Tasks.TaskSourceKind.Workspace, config: { index, element: external, file: '.vscode/tasks.json', workspaceFolder: context.workspaceFolder }, label }; break; } } @@ -2110,7 +2105,7 @@ class ConfigurationParser { const name = Tasks.CommandString.value(globals.command.name); const task: Tasks.CustomTask = new Tasks.CustomTask( context.uuidMap.getUUID(name), - Object.assign({} as Tasks.IWorkspaceTaskSource, source, { config: { index: -1, element: fileConfig, workspaceFolder: context.workspaceFolder } }), + Object.assign({}, source, 'workspace', { config: { index: -1, element: fileConfig, workspaceFolder: context.workspaceFolder } }) satisfies Tasks.IWorkspaceTaskSource, name, Tasks.CUSTOMIZED_TASK_TYPE, { From 2a57bf60e225bd5b22ab4ee219d09b83f3b901d2 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Tue, 7 May 2024 13:58:37 -0700 Subject: [PATCH 031/357] Revert f8c7fec0c29c1f6d2fd65c3b2c779d362fe61d0b (#212203) This seems to break extension install/update --- .../abstractExtensionManagementService.ts | 90 ++++---- .../common/extensionGalleryService.ts | 48 ++-- .../common/extensionManagement.ts | 36 ++- .../node/extensionDownloader.ts | 32 ++- .../node/extensionManagementService.ts | 213 ++++++++---------- 5 files changed, 183 insertions(+), 236 deletions(-) diff --git a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts index 95438e4a764..55a1e34ba60 100644 --- a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts +++ b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts @@ -17,7 +17,7 @@ import { ExtensionManagementError, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementParticipant, IGalleryExtension, ILocalExtension, InstallOperation, IExtensionsControlManifest, StatisticType, isTargetPlatformCompatible, TargetPlatformToString, ExtensionManagementErrorCode, InstallOptions, UninstallOptions, Metadata, InstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, IExtensionManagementService, InstallExtensionInfo, EXTENSION_INSTALL_DEP_PACK_CONTEXT, ExtensionGalleryError, - IProductVersion, ExtensionGalleryErrorCode + IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions, ExtensionKey, getGalleryExtensionId, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionType, IExtensionManifest, isApplicationScopedExtension, TargetPlatform } from 'vs/platform/extensions/common/extensions'; @@ -290,10 +290,26 @@ export abstract class AbstractExtensionManagementService extends Disposable impl // Install extensions in parallel and wait until all extensions are installed / failed await this.joinAllSettled([...installingExtensionsMap.entries()].map(async ([key, { task }]) => { const startTime = new Date().getTime(); - let local: ILocalExtension; try { - local = await task.run(); - await this.joinAllSettled(this.participants.map(participant => participant.postInstall(local, task.source, task.options, CancellationToken.None)), ExtensionManagementErrorCode.PostInstall); + const local = await task.run(); + await this.joinAllSettled(this.participants.map(participant => participant.postInstall(local, task.source, task.options, CancellationToken.None))); + if (!URI.isUri(task.source)) { + const isUpdate = task.operation === InstallOperation.Update; + const durationSinceUpdate = isUpdate ? undefined : (new Date().getTime() - task.source.lastUpdated) / 1000; + reportTelemetry(this.telemetryService, isUpdate ? 'extensionGallery:update' : 'extensionGallery:install', { + extensionData: getGalleryExtensionTelemetryData(task.source), + verificationStatus: task.verificationStatus, + duration: new Date().getTime() - startTime, + durationSinceUpdate + }); + // In web, report extension install statistics explicitly. In Desktop, statistics are automatically updated while downloading the VSIX. + if (isWeb && task.operation !== InstallOperation.Update) { + try { + await this.galleryService.reportStatistic(local.manifest.publisher, local.manifest.name, local.manifest.version, StatisticType.Install); + } catch (error) { /* ignore */ } + } + } + installExtensionResultsMap.set(key, { local, identifier: task.identifier, operation: task.operation, source: task.source, context: task.options.context, profileLocation: task.profileLocation, applicationScoped: local.isApplicationScoped }); } catch (e) { const error = toExtensionManagementError(e); if (!URI.isUri(task.source)) { @@ -303,23 +319,6 @@ export abstract class AbstractExtensionManagementService extends Disposable impl this.logService.error('Error while installing the extension', task.identifier.id, getErrorMessage(error)); throw error; } - if (!URI.isUri(task.source)) { - const isUpdate = task.operation === InstallOperation.Update; - const durationSinceUpdate = isUpdate ? undefined : (new Date().getTime() - task.source.lastUpdated) / 1000; - reportTelemetry(this.telemetryService, isUpdate ? 'extensionGallery:update' : 'extensionGallery:install', { - extensionData: getGalleryExtensionTelemetryData(task.source), - verificationStatus: task.verificationStatus, - duration: new Date().getTime() - startTime, - durationSinceUpdate - }); - // In web, report extension install statistics explicitly. In Desktop, statistics are automatically updated while downloading the VSIX. - if (isWeb && task.operation !== InstallOperation.Update) { - try { - await this.galleryService.reportStatistic(local.manifest.publisher, local.manifest.name, local.manifest.version, StatisticType.Install); - } catch (error) { /* ignore */ } - } - } - installExtensionResultsMap.set(key, { local, identifier: task.identifier, operation: task.operation, source: task.source, context: task.options.context, profileLocation: task.profileLocation, applicationScoped: local.isApplicationScoped }); })); if (alreadyRequestedInstallations.length) { @@ -429,35 +428,36 @@ export abstract class AbstractExtensionManagementService extends Disposable impl return true; } - private async joinAllSettled(promises: Promise[], errorCode?: ExtensionManagementErrorCode): Promise { + private async joinAllSettled(promises: Promise[]): Promise { const results: T[] = []; - const errors: ExtensionManagementError[] = []; + const errors: any[] = []; const promiseResults = await Promise.allSettled(promises); for (const r of promiseResults) { if (r.status === 'fulfilled') { results.push(r.value); } else { - errors.push(toExtensionManagementError(r.reason, errorCode)); + errors.push(r.reason); } } - if (!errors.length) { - return results; - } - // Throw if there are errors - if (errors.length === 1) { - throw errors[0]; + if (errors.length) { + if (errors.length === 1) { + throw errors[0]; + } + + let error = new ExtensionManagementError('', ExtensionManagementErrorCode.Unknown); + for (const current of errors) { + const code = current instanceof ExtensionManagementError ? current.code : ExtensionManagementErrorCode.Unknown; + error = new ExtensionManagementError( + current.message ? `${current.message}, ${error.message}` : error.message, + code !== ExtensionManagementErrorCode.Unknown && code !== ExtensionManagementErrorCode.Internal ? code : error.code + ); + } + throw error; } - let error = new ExtensionManagementError('', ExtensionManagementErrorCode.Unknown); - for (const current of errors) { - error = new ExtensionManagementError( - error.message ? `${error.message}, ${current.message}` : current.message, - current.code !== ExtensionManagementErrorCode.Unknown && current.code !== ExtensionManagementErrorCode.Internal ? current.code : error.code - ); - } - throw error; + return results; } private async getAllDepsAndPackExtensions(extensionIdentifier: IExtensionIdentifier, manifest: IExtensionManifest, getOnlyNewlyAddedFromExtensionPack: boolean, installPreRelease: boolean, profile: URI | undefined, productVersion: IProductVersion): Promise<{ gallery: IGalleryExtension; manifest: IExtensionManifest }[]> { @@ -787,18 +787,18 @@ export abstract class AbstractExtensionManagementService extends Disposable impl protected abstract copyExtension(extension: ILocalExtension, fromProfileLocation: URI, toProfileLocation: URI, metadata?: Partial): Promise; } -export function toExtensionManagementError(error: Error, code?: ExtensionManagementErrorCode): ExtensionManagementError { +export function toExtensionManagementError(error: Error): ExtensionManagementError { if (error instanceof ExtensionManagementError) { return error; } - let extensionManagementError: ExtensionManagementError; if (error instanceof ExtensionGalleryError) { - extensionManagementError = new ExtensionManagementError(error.message, error.code === ExtensionGalleryErrorCode.DownloadFailedWriting ? ExtensionManagementErrorCode.DownloadFailedWriting : ExtensionManagementErrorCode.Gallery); - } else { - extensionManagementError = new ExtensionManagementError(error.message, isCancellationError(error) ? ExtensionManagementErrorCode.Cancelled : (code ?? ExtensionManagementErrorCode.Internal)); + const e = new ExtensionManagementError(error.message, ExtensionManagementErrorCode.Gallery); + e.stack = error.stack; + return e; } - extensionManagementError.stack = error.stack; - return extensionManagementError; + const e = new ExtensionManagementError(error.message, isCancellationError(error) ? ExtensionManagementErrorCode.Cancelled : ExtensionManagementErrorCode.Internal); + e.stack = error.stack; + return e; } function reportTelemetry(telemetryService: ITelemetryService, eventName: string, { extensionData, verificationStatus, duration, error, durationSinceUpdate }: { extensionData: any; verificationStatus?: ExtensionVerificationStatus; duration?: number; durationSinceUpdate?: number; error?: ExtensionManagementError | ExtensionGalleryError }): void { diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index cc587b5770e..ebdb1bb50b8 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -1029,28 +1029,6 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi this.logService.trace('ExtensionGalleryService#download', extension.identifier.id); const data = getGalleryExtensionTelemetryData(extension); const startTime = new Date().getTime(); - - const operationParam = operation === InstallOperation.Install ? 'install' : operation === InstallOperation.Update ? 'update' : ''; - const downloadAsset = operationParam ? { - uri: `${extension.assets.download.uri}${URI.parse(extension.assets.download.uri).query ? '&' : '?'}${operationParam}=true`, - fallbackUri: `${extension.assets.download.fallbackUri}${URI.parse(extension.assets.download.fallbackUri).query ? '&' : '?'}${operationParam}=true` - } : extension.assets.download; - - const headers: IHeaders | undefined = extension.queryContext?.[ACTIVITY_HEADER_NAME] ? { [ACTIVITY_HEADER_NAME]: extension.queryContext[ACTIVITY_HEADER_NAME] } : undefined; - const context = await this.getAsset(extension.identifier.id, downloadAsset, AssetType.VSIX, headers ? { headers } : undefined); - - try { - await this.fileService.writeFile(location, context.stream); - } catch (error) { - try { - await this.fileService.del(location); - } catch (e) { - /* ignore */ - this.logService.warn(`Error while deleting the file ${location.toString()}`, getErrorMessage(e)); - } - throw new ExtensionGalleryError(getErrorMessage(error), ExtensionGalleryErrorCode.DownloadFailedWriting); - } - /* __GDPR__ "galleryService:downloadVSIX" : { "owner": "sandy081", @@ -1060,7 +1038,18 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi ] } */ - this.telemetryService.publicLog('galleryService:downloadVSIX', { ...data, duration: new Date().getTime() - startTime }); + const log = (duration: number) => this.telemetryService.publicLog('galleryService:downloadVSIX', { ...data, duration }); + + const operationParam = operation === InstallOperation.Install ? 'install' : operation === InstallOperation.Update ? 'update' : ''; + const downloadAsset = operationParam ? { + uri: `${extension.assets.download.uri}${URI.parse(extension.assets.download.uri).query ? '&' : '?'}${operationParam}=true`, + fallbackUri: `${extension.assets.download.fallbackUri}${URI.parse(extension.assets.download.fallbackUri).query ? '&' : '?'}${operationParam}=true` + } : extension.assets.download; + + const headers: IHeaders | undefined = extension.queryContext?.[ACTIVITY_HEADER_NAME] ? { [ACTIVITY_HEADER_NAME]: extension.queryContext[ACTIVITY_HEADER_NAME] } : undefined; + const context = await this.getAsset(extension.identifier.id, downloadAsset, AssetType.VSIX, headers ? { headers } : undefined); + await this.fileService.writeFile(location, context.stream); + log(new Date().getTime() - startTime); } async downloadSignatureArchive(extension: IGalleryExtension, location: URI): Promise { @@ -1071,18 +1060,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi this.logService.trace('ExtensionGalleryService#downloadSignatureArchive', extension.identifier.id); const context = await this.getAsset(extension.identifier.id, extension.assets.signature, AssetType.Signature); - try { - await this.fileService.writeFile(location, context.stream); - } catch (error) { - try { - await this.fileService.del(location); - } catch (e) { - /* ignore */ - this.logService.warn(`Error while deleting the file ${location.toString()}`, getErrorMessage(e)); - } - throw new ExtensionGalleryError(getErrorMessage(error), ExtensionGalleryErrorCode.DownloadFailedWriting); - } - + await this.fileService.writeFile(location, context.stream); } async getReadme(extension: IGalleryExtension, token: CancellationToken): Promise { diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index fa9e1b0983d..d69233901b8 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -407,21 +407,7 @@ export interface DidUninstallExtensionEvent { readonly workspaceScoped?: boolean; } -export const enum ExtensionGalleryErrorCode { - Timeout = 'Timeout', - Cancelled = 'Cancelled', - Failed = 'Failed', - DownloadFailedWriting = 'DownloadFailedWriting', -} - -export class ExtensionGalleryError extends Error { - constructor(message: string, readonly code: ExtensionGalleryErrorCode) { - super(message); - this.name = code; - } -} - -export const enum ExtensionManagementErrorCode { +export enum ExtensionManagementErrorCode { Unsupported = 'Unsupported', Deprecated = 'Deprecated', Malicious = 'Malicious', @@ -431,18 +417,11 @@ export const enum ExtensionManagementErrorCode { Invalid = 'Invalid', Download = 'Download', DownloadSignature = 'DownloadSignature', - DownloadFailedWriting = ExtensionGalleryErrorCode.DownloadFailedWriting, UpdateMetadata = 'UpdateMetadata', Extract = 'Extract', Scanning = 'Scanning', - ScanningExtension = 'ScanningExtension', - ReadUninstalled = 'ReadUninstalled', - UnsetUninstalled = 'UnsetUninstalled', Delete = 'Delete', Rename = 'Rename', - IntializeDefaultProfile = 'IntializeDefaultProfile', - AddToProfile = 'AddToProfile', - PostInstall = 'PostInstall', CorruptZip = 'CorruptZip', IncompleteZip = 'IncompleteZip', Signature = 'Signature', @@ -460,6 +439,19 @@ export class ExtensionManagementError extends Error { } } +export enum ExtensionGalleryErrorCode { + Timeout = 'Timeout', + Cancelled = 'Cancelled', + Failed = 'Failed' +} + +export class ExtensionGalleryError extends Error { + constructor(message: string, readonly code: ExtensionGalleryErrorCode) { + super(message); + this.name = code; + } +} + export type InstallOptions = { isBuiltin?: boolean; isWorkspaceScoped?: boolean; diff --git a/src/vs/platform/extensionManagement/node/extensionDownloader.ts b/src/vs/platform/extensionManagement/node/extensionDownloader.ts index d12dc956d44..85fb4668e79 100644 --- a/src/vs/platform/extensionManagement/node/extensionDownloader.ts +++ b/src/vs/platform/extensionManagement/node/extensionDownloader.ts @@ -16,7 +16,7 @@ import { Promises as FSPromises } from 'vs/base/node/pfs'; import { CorruptZipMessage } from 'vs/base/node/zip'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; -import { ExtensionVerificationStatus, toExtensionManagementError } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService'; +import { ExtensionVerificationStatus } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService'; import { ExtensionManagementError, ExtensionManagementErrorCode, IExtensionGalleryService, IGalleryExtension, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionKey, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionSignatureVerificationError, ExtensionSignatureVerificationCode, IExtensionSignatureVerificationService } from 'vs/platform/extensionManagement/node/extensionSignatureVerificationService'; @@ -52,7 +52,7 @@ export class ExtensionsDownloader extends Disposable { try { await this.downloadFile(extension, location, location => this.extensionGalleryService.download(extension, location, operation)); } catch (error) { - throw toExtensionManagementError(error, ExtensionManagementErrorCode.Download); + throw new ExtensionManagementError(error.message, ExtensionManagementErrorCode.Download); } let verificationStatus: ExtensionVerificationStatus = false; @@ -62,20 +62,23 @@ export class ExtensionsDownloader extends Disposable { try { verificationStatus = await this.extensionSignatureVerificationService.verify(extension.identifier.id, location.fsPath, signatureArchiveLocation.fsPath); } catch (error) { - verificationStatus = (error as ExtensionSignatureVerificationError).code; + const sigError = error as ExtensionSignatureVerificationError; + verificationStatus = sigError.code; if (verificationStatus === ExtensionSignatureVerificationCode.PackageIsInvalidZip || verificationStatus === ExtensionSignatureVerificationCode.SignatureArchiveIsInvalidZip) { + try { + // Delete the downloaded vsix before throwing the error + await this.delete(location); + } catch (error) { + this.logService.error(error); + } throw new ExtensionManagementError(CorruptZipMessage, ExtensionManagementErrorCode.CorruptZip); } } finally { try { - // Delete downloaded files - await Promise.allSettled([ - this.delete(location), - this.delete(signatureArchiveLocation) - ]); + // Delete signature archive always + await this.delete(signatureArchiveLocation); } catch (error) { - // Ignore error - this.logService.warn(`Error while deleting downloaded files: ${getErrorMessage(error)}`); + this.logService.error(error); } } } @@ -100,7 +103,7 @@ export class ExtensionsDownloader extends Disposable { try { await this.downloadFile(extension, location, location => this.extensionGalleryService.downloadSignatureArchive(extension, location)); } catch (error) { - throw toExtensionManagementError(error, ExtensionManagementErrorCode.DownloadSignature); + throw new ExtensionManagementError(error.message, ExtensionManagementErrorCode.DownloadSignature); } return location; } @@ -119,13 +122,8 @@ export class ExtensionsDownloader extends Disposable { // Download to temporary location first only if file does not exist const tempLocation = joinPath(this.extensionsDownloadDir, `.${generateUuid()}`); - try { + if (!await this.fileService.exists(tempLocation)) { await downloadFn(tempLocation); - } catch (error) { - try { - await this.fileService.del(tempLocation); - } catch (e) { /* ignore */ } - throw error; } try { diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 122a6ed9b7b..53630b739fe 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -451,28 +451,20 @@ export class ExtensionsScanner extends Disposable { } async scanExtensions(type: ExtensionType | null, profileLocation: URI, productVersion: IProductVersion): Promise { - try { - const userScanOptions: ScanOptions = { includeInvalid: true, profileLocation, productVersion }; - let scannedExtensions: IScannedExtension[] = []; - if (type === null || type === ExtensionType.System) { - scannedExtensions.push(...await this.extensionsScannerService.scanAllExtensions({ includeInvalid: true }, userScanOptions, false)); - } else if (type === ExtensionType.User) { - scannedExtensions.push(...await this.extensionsScannerService.scanUserExtensions(userScanOptions)); - } - scannedExtensions = type !== null ? scannedExtensions.filter(r => r.type === type) : scannedExtensions; - return await Promise.all(scannedExtensions.map(extension => this.toLocalExtension(extension))); - } catch (error) { - throw toExtensionManagementError(error, ExtensionManagementErrorCode.Scanning); + const userScanOptions: ScanOptions = { includeInvalid: true, profileLocation, productVersion }; + let scannedExtensions: IScannedExtension[] = []; + if (type === null || type === ExtensionType.System) { + scannedExtensions.push(...await this.extensionsScannerService.scanAllExtensions({ includeInvalid: true }, userScanOptions, false)); + } else if (type === ExtensionType.User) { + scannedExtensions.push(...await this.extensionsScannerService.scanUserExtensions(userScanOptions)); } + scannedExtensions = type !== null ? scannedExtensions.filter(r => r.type === type) : scannedExtensions; + return Promise.all(scannedExtensions.map(extension => this.toLocalExtension(extension))); } async scanAllUserExtensions(excludeOutdated: boolean): Promise { - try { - const scannedExtensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: !excludeOutdated, includeInvalid: true }); - return await Promise.all(scannedExtensions.map(extension => this.toLocalExtension(extension))); - } catch (error) { - throw toExtensionManagementError(error, ExtensionManagementErrorCode.Scanning); - } + const scannedExtensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: !excludeOutdated, includeInvalid: true }); + return Promise.all(scannedExtensions.map(extension => this.toLocalExtension(extension))); } async scanUserExtensionAtLocation(location: URI): Promise { @@ -504,11 +496,7 @@ export class ExtensionsScanner extends Disposable { } if (exists) { - try { - await this.extensionsScannerService.updateMetadata(extensionLocation, metadata); - } catch (error) { - throw toExtensionManagementError(error, ExtensionManagementErrorCode.UpdateMetadata); - } + await this.extensionsScannerService.updateMetadata(extensionLocation, metadata); } else { try { // Extract @@ -525,14 +513,10 @@ export class ExtensionsScanner extends Disposable { errorCode = ExtensionManagementErrorCode.IncompleteZip; } } - throw toExtensionManagementError(e, errorCode); + throw new ExtensionManagementError(e.message, errorCode); } - try { - await this.extensionsScannerService.updateMetadata(tempLocation, metadata); - } catch (error) { - throw toExtensionManagementError(error, ExtensionManagementErrorCode.UpdateMetadata); - } + await this.extensionsScannerService.updateMetadata(tempLocation, metadata); // Rename try { @@ -574,24 +558,16 @@ export class ExtensionsScanner extends Disposable { } async updateMetadata(local: ILocalExtension, metadata: Partial, profileLocation?: URI): Promise { - try { - if (profileLocation) { - await this.extensionsProfileScannerService.updateMetadata([[local, metadata]], profileLocation); - } else { - await this.extensionsScannerService.updateMetadata(local.location, metadata); - } - } catch (error) { - throw toExtensionManagementError(error, ExtensionManagementErrorCode.UpdateMetadata); + if (profileLocation) { + await this.extensionsProfileScannerService.updateMetadata([[local, metadata]], profileLocation); + } else { + await this.extensionsScannerService.updateMetadata(local.location, metadata); } return this.scanLocalExtension(local.location, local.type, profileLocation); } - async getUninstalledExtensions(): Promise> { - try { - return await this.withUninstalledExtensions(); - } catch (error) { - throw toExtensionManagementError(error, ExtensionManagementErrorCode.ReadUninstalled); - } + getUninstalledExtensions(): Promise> { + return this.withUninstalledExtensions(); } async setUninstalled(...extensions: IExtension[]): Promise { @@ -604,11 +580,7 @@ export class ExtensionsScanner extends Disposable { } async setInstalled(extensionKey: ExtensionKey): Promise { - try { - await this.withUninstalledExtensions(uninstalled => delete uninstalled[extensionKey.toString()]); - } catch (error) { - throw toExtensionManagementError(error, ExtensionManagementErrorCode.UnsetUninstalled); - } + await this.withUninstalledExtensions(uninstalled => delete uninstalled[extensionKey.toString()]); } async removeExtension(extension: ILocalExtension | IScannedExtension, type: string): Promise { @@ -658,7 +630,7 @@ export class ExtensionsScanner extends Disposable { this.logService.info(`Deleted ${type} extension from disk`, id, location.fsPath); } - private withUninstalledExtensions(updateFn?: (uninstalled: IStringDictionary) => void): Promise> { + private async withUninstalledExtensions(updateFn?: (uninstalled: IStringDictionary) => void): Promise> { return this.uninstalledFileLimiter.queue(async () => { let raw: string | undefined; try { @@ -694,28 +666,24 @@ export class ExtensionsScanner extends Disposable { try { await pfs.Promises.rename(extractPath, renamePath, 2 * 60 * 1000 /* Retry for 2 minutes */); } catch (error) { - throw toExtensionManagementError(error, ExtensionManagementErrorCode.Rename); + throw new ExtensionManagementError(error.message || nls.localize('renameError', "Unknown error while renaming {0} to {1}", extractPath, renamePath), error.code || ExtensionManagementErrorCode.Rename); } } private async scanLocalExtension(location: URI, type: ExtensionType, profileLocation?: URI): Promise { - try { - if (profileLocation) { - const scannedExtensions = await this.extensionsScannerService.scanUserExtensions({ profileLocation }); - const scannedExtension = scannedExtensions.find(e => this.uriIdentityService.extUri.isEqual(e.location, location)); - if (scannedExtension) { - return await this.toLocalExtension(scannedExtension); - } - } else { - const scannedExtension = await this.extensionsScannerService.scanExistingExtension(location, type, { includeInvalid: true }); - if (scannedExtension) { - return await this.toLocalExtension(scannedExtension); - } + if (profileLocation) { + const scannedExtensions = await this.extensionsScannerService.scanUserExtensions({ profileLocation }); + const scannedExtension = scannedExtensions.find(e => this.uriIdentityService.extUri.isEqual(e.location, location)); + if (scannedExtension) { + return this.toLocalExtension(scannedExtension); + } + } else { + const scannedExtension = await this.extensionsScannerService.scanExistingExtension(location, type, { includeInvalid: true }); + if (scannedExtension) { + return this.toLocalExtension(scannedExtension); } - throw new ExtensionManagementError(nls.localize('cannot read', "Cannot read the extension from {0}", location.path), ExtensionManagementErrorCode.ScanningExtension); - } catch (error) { - throw toExtensionManagementError(error, ExtensionManagementErrorCode.ScanningExtension); } + throw new Error(nls.localize('cannot read', "Cannot read the extension from {0}", location.path)); } private async toLocalExtension(extension: IScannedExtension): Promise { @@ -853,17 +821,9 @@ abstract class InstallExtensionTask extends AbstractExtensionTask { - const uninstalled = await this.extensionsScanner.getUninstalledExtensions(); - if (!uninstalled[extensionKey.toString()]) { + const isUninstalled = await this.isUninstalled(extensionKey); + if (!isUninstalled) { return undefined; } @@ -894,6 +854,11 @@ abstract class InstallExtensionTask extends AbstractExtensionTask ExtensionKey.create(i).equals(extensionKey)); } + private async isUninstalled(extensionId: ExtensionKey): Promise { + const uninstalled = await this.extensionsScanner.getUninstalledExtensions(); + return !!uninstalled[extensionId.toString()]; + } + protected abstract install(token: CancellationToken): Promise<[ILocalExtension, Metadata]>; } @@ -917,7 +882,13 @@ export class InstallGalleryExtensionTask extends InstallExtensionTask { } protected async install(token: CancellationToken): Promise<[ILocalExtension, Metadata]> { - const installed = await this.extensionsScanner.scanExtensions(null, this.options.profileLocation, this.options.productVersion); + let installed; + try { + installed = await this.extensionsScanner.scanExtensions(null, this.options.profileLocation, this.options.productVersion); + } catch (error) { + throw new ExtensionManagementError(error, ExtensionManagementErrorCode.Scanning); + } + const existingExtension = installed.find(i => areSameExtensions(i.identifier, this.gallery.identifier)); if (existingExtension) { this._operation = InstallOperation.Update; @@ -944,64 +915,72 @@ export class InstallGalleryExtensionTask extends InstallExtensionTask { }; if (existingExtension && existingExtension.type !== ExtensionType.System && existingExtension.manifest.version === this.gallery.version) { - const local = await this.extensionsScanner.updateMetadata(existingExtension, metadata); - return [local, metadata]; + try { + const local = await this.extensionsScanner.updateMetadata(existingExtension, metadata); + return [local, metadata]; + } catch (error) { + throw new ExtensionManagementError(getErrorMessage(error), ExtensionManagementErrorCode.UpdateMetadata); + } } - const { verificationStatus, location } = await this.download(metadata, token); + try { + return await this.downloadAndInstallExtension(metadata, token); + } catch (error) { + if (error instanceof ExtensionManagementError && (error.code === ExtensionManagementErrorCode.CorruptZip || error.code === ExtensionManagementErrorCode.IncompleteZip)) { + this.logService.info(`Downloaded VSIX is invalid. Trying to download and install again...`, this.gallery.identifier.id); + type RetryInstallingInvalidVSIXClassification = { + owner: 'sandy081'; + comment: 'Event reporting the retry of installing an invalid VSIX'; + extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Extension Id' }; + succeeded?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Success value' }; + }; + type RetryInstallingInvalidVSIXEvent = { + extensionId: string; + succeeded: boolean; + }; + try { + const result = await this.downloadAndInstallExtension(metadata, token); + this.telemetryService.publicLog2('extensiongallery:install:retry', { + extensionId: this.gallery.identifier.id, + succeeded: true + }); + return result; + } catch (error) { + this.telemetryService.publicLog2('extensiongallery:install:retry', { + extensionId: this.gallery.identifier.id, + succeeded: false + }); + throw error; + } + } else { + throw error; + } + } + } + + private async downloadAndInstallExtension(metadata: Metadata, token: CancellationToken): Promise<[ILocalExtension, Metadata]> { + const { location, verificationStatus } = await this.extensionsDownloader.download(this.gallery, this._operation, !this.options.donotVerifySignature); try { this._verificationStatus = verificationStatus; - await this.validateManifest(location.fsPath); + this.validateManifest(location.fsPath); const local = await this.extractExtension({ zipPath: location.fsPath, key: ExtensionKey.create(this.gallery), metadata }, false, token); return [local, metadata]; } catch (error) { try { await this.extensionsDownloader.delete(location); - } catch (e) { + } catch (error) { /* Ignore */ - this.logService.warn(`Error while deleting the downloaded file`, location.toString(), getErrorMessage(e)); + this.logService.warn(`Error while deleting the downloaded file`, location.toString(), getErrorMessage(error)); } throw error; } } - private async download(metadata: Metadata, token: CancellationToken): Promise<{ readonly location: URI; readonly verificationStatus: ExtensionVerificationStatus }> { - try { - return await this.extensionsDownloader.download(this.gallery, this._operation, !this.options.donotVerifySignature); - } catch (error) { - this.logService.info(`Failed downloading. Retry again...`, this.gallery.identifier.id); - type RetryDownloadingVSIXClassification = { - owner: 'sandy081'; - comment: 'Event reporting the retry of downloading the VSIX'; - extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Extension Id' }; - succeeded?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Success value' }; - }; - type RetryDownloadingVSIXEvent = { - extensionId: string; - succeeded: boolean; - }; - try { - const result = await this.download(metadata, token); - this.telemetryService.publicLog2('extensiongallery:download:retry', { - extensionId: this.gallery.identifier.id, - succeeded: true - }); - return result; - } catch (error) { - this.telemetryService.publicLog2('extensiongallery:download:retry', { - extensionId: this.gallery.identifier.id, - succeeded: false - }); - throw error; - } - } - } - protected async validateManifest(zipPath: string): Promise { try { await getManifest(zipPath); } catch (error) { - throw toExtensionManagementError(error, ExtensionManagementErrorCode.Invalid); + throw new ExtensionManagementError(error.message, ExtensionManagementErrorCode.Invalid); } } From 6e69d8b462ff61e8428c5d30d479e7477f04372a Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 7 May 2024 16:15:34 -0700 Subject: [PATCH 032/357] testing: initial attributable test coverage API This implements the data flow for attributable test coverage. Todo for tomorrow is to support this in our test runner (probably making module to generate per-test coverage for generic JS tests) and then building some UX for it. --- src/vs/base/common/assert.ts | 4 +- src/vs/base/common/prefixTree.ts | 5 + src/vs/workbench/api/common/extHostTesting.ts | 55 +++-- .../api/common/extHostTypeConverters.ts | 4 +- src/vs/workbench/api/common/extHostTypes.ts | 1 + .../api/test/browser/extHostTesting.test.ts | 33 +-- .../contrib/testing/common/testCoverage.ts | 72 +++++-- .../contrib/testing/common/testTypes.ts | 11 + .../testing/test/common/testCoverage.test.ts | 194 ++++++++++++++++++ .../common/extensionsApiProposals.ts | 1 + .../vscode.proposed.attributableCoverage.d.ts | 28 +++ 11 files changed, 359 insertions(+), 49 deletions(-) create mode 100644 src/vs/workbench/contrib/testing/test/common/testCoverage.test.ts create mode 100644 src/vscode-dts/vscode.proposed.attributableCoverage.d.ts diff --git a/src/vs/base/common/assert.ts b/src/vs/base/common/assert.ts index 4ded48fb1de..bbd344d55c7 100644 --- a/src/vs/base/common/assert.ts +++ b/src/vs/base/common/assert.ts @@ -29,9 +29,9 @@ export function assertNever(value: never, message = 'Unreachable'): never { throw new Error(message); } -export function assert(condition: boolean): void { +export function assert(condition: boolean, message = 'unexpected state'): asserts condition { if (!condition) { - throw new BugIndicatingError('Assertion Failed'); + throw new BugIndicatingError(`Assertion Failed: ${message}`); } } diff --git a/src/vs/base/common/prefixTree.ts b/src/vs/base/common/prefixTree.ts index 0c2bdca384b..8e839e2b4ff 100644 --- a/src/vs/base/common/prefixTree.ts +++ b/src/vs/base/common/prefixTree.ts @@ -46,6 +46,11 @@ export class WellDefinedPrefixTree { this.opNode(key, n => n._value = mutate(n._value === unset ? undefined : n._value)); } + /** Mutates nodes along the path in the prefix tree. */ + mutatePath(key: Iterable, mutate: (node: IPrefixTreeNode) => void): void { + this.opNode(key, () => { }, n => mutate(n)); + } + /** Deletes a node from the prefix tree, returning the value it contained. */ delete(key: Iterable): V | undefined { const path = this.getPathToKey(key); diff --git a/src/vs/workbench/api/common/extHostTesting.ts b/src/vs/workbench/api/common/extHostTesting.ts index 44a28328eab..7e50f1fa9fc 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/src/vs/workbench/api/common/extHostTesting.ts @@ -23,11 +23,12 @@ import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocum import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { ExtHostTestItemCollection, TestItemImpl, TestItemRootImpl, toItemFromContext } from 'vs/workbench/api/common/extHostTestItem'; import * as Convert from 'vs/workbench/api/common/extHostTypeConverters'; -import { TestRunProfileKind, TestRunRequest } from 'vs/workbench/api/common/extHostTypes'; +import { TestRunProfileKind, TestRunRequest, FileCoverage } from 'vs/workbench/api/common/extHostTypes'; import { TestCommandId } from 'vs/workbench/contrib/testing/common/constants'; import { TestId, TestIdPathParts, TestPosition } from 'vs/workbench/contrib/testing/common/testId'; import { InvalidTestItemError } from 'vs/workbench/contrib/testing/common/testItemCollection'; import { AbstractIncrementalTestCollection, CoverageDetails, ICallProfileRunHandler, ISerializedTestResults, IStartControllerTests, IStartControllerTestsResult, ITestErrorMessage, ITestItem, ITestItemContext, ITestMessageMenuArgs, ITestRunProfile, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, TestResultState, TestRunProfileBitset, TestsDiff, TestsDiffOp, isStartControllerTests } from 'vs/workbench/contrib/testing/common/testTypes'; +import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import type * as vscode from 'vscode'; interface ControllerInfo { @@ -154,7 +155,7 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { return new TestItemImpl(controllerId, id, label, uri); }, createTestRun: (request, name, persist = true) => { - return this.runTracker.createTestRun(controllerId, collection, request, name, persist); + return this.runTracker.createTestRun(extension, controllerId, collection, request, name, persist); }, invalidateTestResults: items => { if (items === undefined) { @@ -353,7 +354,7 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { return {}; } - const { collection, profiles } = lookup; + const { collection, profiles, extension } = lookup; const profile = profiles.get(req.profileId); if (!profile) { return {}; @@ -382,6 +383,7 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { ); const tracker = isStartControllerTests(req) && this.runTracker.prepareForMainThreadTestRun( + extension, publicReq, TestRunDto.fromInternal(req, lookup.collection), profile, @@ -460,6 +462,7 @@ class TestRunTracker extends Disposable { private readonly proxy: MainThreadTestingShape, private readonly logService: ILogService, private readonly profile: vscode.TestRunProfile | undefined, + private readonly extension: IRelaxedExtensionDescription, parentToken?: CancellationToken, ) { super(); @@ -539,16 +542,43 @@ class TestRunTracker extends Disposable { }; let ended = false; + + // one-off map used to associate test items with incrementing IDs in `addCoverage`. + // There's no need to include their entire ID, we just want to make sure they're + // stable and unique. Normal map is okay since TestRun lifetimes are limited. + const testItemCoverageId = new Map(); const run: vscode.TestRun = { isPersisted: this.dto.isPersisted, token: this.cts.token, name, onDidDispose: this.onDidDispose, - addCoverage: coverage => { + addCoverage: (coverage) => { + if (ended) { + return; + } + + const testItem = coverage instanceof FileCoverage ? coverage.testItem : undefined; + let testItemIdPart: undefined | number; + if (testItem) { + checkProposedApiEnabled(this.extension, 'attributableCoverage'); + if (!this.dto.isIncluded(testItem)) { + throw new Error('Attempted to `addCoverage` for a test item not included in the run'); + } + + testItemIdPart = testItemCoverageId.get(testItem); + if (testItemIdPart === undefined) { + testItemIdPart = testItemCoverageId.size; + testItemCoverageId.set(testItem, testItemIdPart); + } + } + const uriStr = coverage.uri.toString(); - const id = new TestId([runId, taskId, uriStr]).toString(); + const id = new TestId(testItemIdPart + ? [runId, taskId, uriStr, String(testItemIdPart)] + : [runId, taskId, uriStr], + ).toString(); this.publishedCoverage.set(uriStr, coverage); - this.proxy.$appendCoverage(runId, taskId, Convert.TestCoverage.fromFile(id, coverage)); + this.proxy.$appendCoverage(runId, taskId, Convert.TestCoverage.fromFile(ctrlId, id, coverage)); }, //#region state mutation enqueued: guardTestMutation(test => { @@ -599,6 +629,7 @@ class TestRunTracker extends Disposable { } ended = true; + testItemCoverageId.clear(); this.proxy.$finishedTestRunTask(runId, taskId); if (!--this.running) { this.markEnded(); @@ -706,8 +737,8 @@ export class TestRunCoordinator { * `$startedExtensionTestRun` is not invoked. The run must eventually * be cancelled manually. */ - public prepareForMainThreadTestRun(req: vscode.TestRunRequest, dto: TestRunDto, profile: vscode.TestRunProfile, token: CancellationToken) { - return this.getTracker(req, dto, profile, token); + public prepareForMainThreadTestRun(extension: IRelaxedExtensionDescription, req: vscode.TestRunRequest, dto: TestRunDto, profile: vscode.TestRunProfile, token: CancellationToken) { + return this.getTracker(req, dto, profile, extension, token); } /** @@ -729,7 +760,7 @@ export class TestRunCoordinator { /** * Implements the public `createTestRun` API. */ - public createTestRun(controllerId: string, collection: ExtHostTestItemCollection, request: vscode.TestRunRequest, name: string | undefined, persist: boolean): vscode.TestRun { + public createTestRun(extension: IRelaxedExtensionDescription, controllerId: string, collection: ExtHostTestItemCollection, request: vscode.TestRunRequest, name: string | undefined, persist: boolean): vscode.TestRun { const existing = this.tracked.get(request); if (existing) { return existing.createRun(name); @@ -750,7 +781,7 @@ export class TestRunCoordinator { persist }); - const tracker = this.getTracker(request, dto, request.profile); + const tracker = this.getTracker(request, dto, request.profile, extension); Event.once(tracker.onEnd)(() => { this.proxy.$finishedExtensionTestRun(dto.id); }); @@ -758,8 +789,8 @@ export class TestRunCoordinator { return tracker.createRun(name); } - private getTracker(req: vscode.TestRunRequest, dto: TestRunDto, profile: vscode.TestRunProfile | undefined, token?: CancellationToken) { - const tracker = new TestRunTracker(dto, this.proxy, this.logService, profile, token); + private getTracker(req: vscode.TestRunRequest, dto: TestRunDto, profile: vscode.TestRunProfile | undefined, extension: IRelaxedExtensionDescription, token?: CancellationToken) { + const tracker = new TestRunTracker(dto, this.proxy, this.logService, profile, extension, token); this.tracked.set(req, tracker); this.trackedById.set(tracker.id, tracker); return tracker; diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 6d3beb0e99a..defa6ec84bf 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -2052,7 +2052,7 @@ export namespace TestCoverage { } } - export function fromFile(id: string, coverage: vscode.FileCoverage): IFileCoverage.Serialized { + export function fromFile(controllerId: string, id: string, coverage: vscode.FileCoverage): IFileCoverage.Serialized { types.validateTestCoverageCount(coverage.statementCoverage); types.validateTestCoverageCount(coverage.branchCoverage); types.validateTestCoverageCount(coverage.declarationCoverage); @@ -2063,6 +2063,8 @@ export namespace TestCoverage { statement: fromCoverageCount(coverage.statementCoverage), branch: coverage.branchCoverage && fromCoverageCount(coverage.branchCoverage), declaration: coverage.declarationCoverage && fromCoverageCount(coverage.declarationCoverage), + testId: coverage instanceof types.FileCoverage && coverage.testItem ? + TestId.fromExtHostTestItem(coverage.testItem, controllerId).toString() : undefined, }; } } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 220035f98f0..9637b4086a2 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -4119,6 +4119,7 @@ export class FileCoverage implements vscode.FileCoverage { public statementCoverage: vscode.TestCoverageCount, public branchCoverage?: vscode.TestCoverageCount, public declarationCoverage?: vscode.TestCoverageCount, + public testItem?: vscode.TestItem, ) { } } diff --git a/src/vs/workbench/api/test/browser/extHostTesting.test.ts b/src/vs/workbench/api/test/browser/extHostTesting.test.ts index 0a4723049b6..b82376cd88d 100644 --- a/src/vs/workbench/api/test/browser/extHostTesting.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTesting.test.ts @@ -14,7 +14,7 @@ import { URI } from 'vs/base/common/uri'; import { mock, mockObject, MockObject } from 'vs/base/test/common/mock'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import * as editorRange from 'vs/editor/common/core/range'; -import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { NullLogService } from 'vs/platform/log/common/log'; import { MainThreadTestingShape } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; @@ -637,6 +637,7 @@ suite('ExtHost Testing', () => { let req: TestRunRequest; let dto: TestRunDto; + const ext: IRelaxedExtensionDescription = {} as any; teardown(() => { for (const { id } of c.trackers) { @@ -671,11 +672,11 @@ suite('ExtHost Testing', () => { }); test('tracks a run started from a main thread request', () => { - const tracker = ds.add(c.prepareForMainThreadTestRun(req, dto, configuration, cts.token)); + const tracker = ds.add(c.prepareForMainThreadTestRun(ext, req, dto, configuration, cts.token)); assert.strictEqual(tracker.hasRunningTasks, false); - const task1 = c.createTestRun('ctrl', single, req, 'run1', true); - const task2 = c.createTestRun('ctrl', single, req, 'run2', true); + const task1 = c.createTestRun(ext, 'ctrl', single, req, 'run1', true); + const task2 = c.createTestRun(ext, 'ctrl', single, req, 'run2', true); assert.strictEqual(proxy.$startedExtensionTestRun.called, false); assert.strictEqual(tracker.hasRunningTasks, true); @@ -696,8 +697,8 @@ suite('ExtHost Testing', () => { test('run cancel force ends after a timeout', () => { const clock = sinon.useFakeTimers(); try { - const tracker = ds.add(c.prepareForMainThreadTestRun(req, dto, configuration, cts.token)); - const task = c.createTestRun('ctrl', single, req, 'run1', true); + const tracker = ds.add(c.prepareForMainThreadTestRun(ext, req, dto, configuration, cts.token)); + const task = c.createTestRun(ext, 'ctrl', single, req, 'run1', true); const onEnded = sinon.stub(); ds.add(tracker.onEnd(onEnded)); @@ -721,8 +722,8 @@ suite('ExtHost Testing', () => { }); test('run cancel force ends on second cancellation request', () => { - const tracker = ds.add(c.prepareForMainThreadTestRun(req, dto, configuration, cts.token)); - const task = c.createTestRun('ctrl', single, req, 'run1', true); + const tracker = ds.add(c.prepareForMainThreadTestRun(ext, req, dto, configuration, cts.token)); + const task = c.createTestRun(ext, 'ctrl', single, req, 'run1', true); const onEnded = sinon.stub(); ds.add(tracker.onEnd(onEnded)); @@ -740,7 +741,7 @@ suite('ExtHost Testing', () => { }); test('tracks a run started from an extension request', () => { - const task1 = c.createTestRun('ctrl', single, req, 'hello world', false); + const task1 = c.createTestRun(ext, 'ctrl', single, req, 'hello world', false); const tracker = Iterable.first(c.trackers)!; assert.strictEqual(tracker.hasRunningTasks, true); @@ -757,8 +758,8 @@ suite('ExtHost Testing', () => { }] ]); - const task2 = c.createTestRun('ctrl', single, req, 'run2', true); - const task3Detached = c.createTestRun('ctrl', single, { ...req }, 'task3Detached', true); + const task2 = c.createTestRun(ext, 'ctrl', single, req, 'run2', true); + const task3Detached = c.createTestRun(ext, 'ctrl', single, { ...req }, 'task3Detached', true); task1.end(); assert.strictEqual(proxy.$finishedExtensionTestRun.called, false); @@ -772,7 +773,7 @@ suite('ExtHost Testing', () => { }); test('adds tests to run smartly', () => { - const task1 = c.createTestRun('ctrlId', single, req, 'hello world', false); + const task1 = c.createTestRun(ext, 'ctrlId', single, req, 'hello world', false); const tracker = Iterable.first(c.trackers)!; const expectedArgs: unknown[][] = []; assert.deepStrictEqual(proxy.$addTestsToRun.args, expectedArgs); @@ -811,7 +812,7 @@ suite('ExtHost Testing', () => { const test2 = new TestItemImpl('ctrlId', 'id-d', 'test d', URI.file('/testd.txt')); test1.range = test2.range = new Range(new Position(0, 0), new Position(1, 0)); single.root.children.replace([test1, test2]); - const task = c.createTestRun('ctrlId', single, req, 'hello world', false); + const task = c.createTestRun(ext, 'ctrlId', single, req, 'hello world', false); const message1 = new TestMessage('some message'); message1.location = new Location(URI.file('/a.txt'), new Position(0, 0)); @@ -852,7 +853,7 @@ suite('ExtHost Testing', () => { }); test('guards calls after runs are ended', () => { - const task = c.createTestRun('ctrl', single, req, 'hello world', false); + const task = c.createTestRun(ext, 'ctrl', single, req, 'hello world', false); task.end(); task.failed(single.root, new TestMessage('some message')); @@ -864,7 +865,7 @@ suite('ExtHost Testing', () => { }); test('excludes tests outside tree or explicitly excluded', () => { - const task = c.createTestRun('ctrlId', single, { + const task = c.createTestRun(ext, 'ctrlId', single, { profile: configuration, include: [single.root.children.get('id-a')!], exclude: [single.root.children.get('id-a')!.children.get('id-aa')!], @@ -894,7 +895,7 @@ suite('ExtHost Testing', () => { const childB = new TestItemImpl('ctrlId', 'id-child', 'child', undefined); testB!.children.replace([childB]); - const task1 = c.createTestRun('ctrl', single, new TestRunRequestImpl(), 'hello world', false); + const task1 = c.createTestRun(ext, 'ctrl', single, new TestRunRequestImpl(), 'hello world', false); const tracker = Iterable.first(c.trackers)!; task1.passed(childA); diff --git a/src/vs/workbench/contrib/testing/common/testCoverage.ts b/src/vs/workbench/contrib/testing/common/testCoverage.ts index 10d4f23cea3..ada6359fded 100644 --- a/src/vs/workbench/contrib/testing/common/testCoverage.ts +++ b/src/vs/workbench/contrib/testing/common/testCoverage.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { assert } from 'vs/base/common/assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ResourceMap } from 'vs/base/common/map'; import { deepClone } from 'vs/base/common/objects'; @@ -34,8 +35,7 @@ export class TestCoverage { private readonly accessor: ICoverageAccessor, ) { } - public append(rawCoverage: IFileCoverage, tx: ITransaction | undefined) { - const coverage = new FileCoverage(rawCoverage, this.accessor); + public append(coverage: IFileCoverage, tx: ITransaction | undefined) { const previous = this.getComputedForUri(coverage.uri); const applyDelta = (kind: 'statement' | 'branch' | 'declaration', node: ComputedFileCoverage) => { if (!node[kind]) { @@ -53,27 +53,51 @@ export class TestCoverage { // version. const canonical = [...this.treePathForUri(coverage.uri, /* canonical = */ true)]; const chain: IPrefixTreeNode[] = []; - this.tree.insert(this.treePathForUri(coverage.uri, /* canonical = */ false), coverage, node => { + const isPerTestCoverage = !!coverage.testId; + this.tree.mutatePath(this.treePathForUri(coverage.uri, /* canonical = */ false), node => { chain.push(node); if (chain.length === canonical.length) { - node.value = coverage; - } else if (!node.value) { - // clone because later intersertions can modify the counts: - const intermediate = deepClone(rawCoverage); - intermediate.id = String(incId++); - intermediate.uri = this.treePathToUri(canonical.slice(0, chain.length)); - node.value = new ComputedFileCoverage(intermediate); - } else { - applyDelta('statement', node.value); - applyDelta('branch', node.value); - applyDelta('declaration', node.value); - node.value.didChange.trigger(tx); + // we reached our destination node, apply the coverage as necessary: + if (isPerTestCoverage) { + const v = node.value ??= new FileCoverage(IFileCoverage.empty(String(incId++), coverage.uri), this.accessor); + assert(v instanceof FileCoverage, 'coverage is unexpectedly computed'); + v.perTestData ??= new Map(); + v.perTestData.set(coverage.testId!.toString(), new FileCoverage(coverage, this.accessor)); + this.fileCoverage.set(coverage.uri, v); + } else if (node.value) { + const v = node.value; + // if ID was generated from a test-specific coverage, reassign it to get its real ID in the extension host. + v.id = coverage.id; + v.statement = coverage.statement; + v.branch = coverage.branch; + v.declaration = coverage.declaration; + v.existsInExtHost = true; + } else { + const v = node.value = new FileCoverage(coverage, this.accessor); + v.existsInExtHost = true; + this.fileCoverage.set(coverage.uri, v); + } + } else if (!isPerTestCoverage) { + // Otherwise, if this is not a partial per-test coverage, merge the + // coverage changes into the chain. Per-test coverages are not complete + // and we don't want to consider them for computation. + if (!node.value) { + // clone because later intersertions can modify the counts: + const intermediate = deepClone(coverage); + intermediate.id = String(incId++); + intermediate.uri = this.treePathToUri(canonical.slice(0, chain.length)); + node.value = new ComputedFileCoverage(intermediate); + } else { + applyDelta('statement', node.value); + applyDelta('branch', node.value); + applyDelta('declaration', node.value); + node.value.didChange.trigger(tx); + } } }); - this.fileCoverage.set(coverage.uri, coverage); - if (chain) { + if (chain && !isPerTestCoverage) { this.didAddCoverage.trigger(tx, chain); } } @@ -131,13 +155,20 @@ export const getTotalCoveragePercent = (statement: ICoverageCount, branch: ICove }; export abstract class AbstractFileCoverage { - public readonly id: string; + public id: string; public readonly uri: URI; public statement: ICoverageCount; public branch?: ICoverageCount; public declaration?: ICoverageCount; public readonly didChange = observableSignal(this); + /** + * Whether this coverage item exists in the extension host. This is false + * if we have only {@link perTestData} and not summary data for the file, or + * if the node is computed for a directory. + */ + public existsInExtHost = false; + /** * Gets the total coverage percent based on information provided. * This is based on the Clover total coverage formula @@ -170,6 +201,11 @@ export class FileCoverage extends AbstractFileCoverage { return this._details instanceof Array || this.resolved; } + /** + * Per-test coverage data for this file, if available. + */ + public perTestData?: Map; + constructor(coverage: IFileCoverage, private readonly accessor: ICoverageAccessor) { super(coverage); } diff --git a/src/vs/workbench/contrib/testing/common/testTypes.ts b/src/vs/workbench/contrib/testing/common/testTypes.ts index cd14796eb0a..d1f779a475d 100644 --- a/src/vs/workbench/contrib/testing/common/testTypes.ts +++ b/src/vs/workbench/contrib/testing/common/testTypes.ts @@ -559,6 +559,7 @@ export namespace ICoverageCount { export interface IFileCoverage { id: string; uri: URI; + testId?: TestId; statement: ICoverageCount; branch?: ICoverageCount; declaration?: ICoverageCount; @@ -568,6 +569,7 @@ export namespace IFileCoverage { export interface Serialized { id: string; uri: UriComponents; + testId: string | undefined; statement: ICoverageCount; branch?: ICoverageCount; declaration?: ICoverageCount; @@ -578,6 +580,7 @@ export namespace IFileCoverage { statement: original.statement, branch: original.branch, declaration: original.declaration, + testId: original.testId?.toString(), uri: original.uri.toJSON(), }); @@ -586,8 +589,16 @@ export namespace IFileCoverage { statement: serialized.statement, branch: serialized.branch, declaration: serialized.declaration, + testId: serialized.testId ? TestId.fromString(serialized.testId) : undefined, uri: uriIdentity.asCanonicalUri(URI.revive(serialized.uri)), }); + + export const empty = (id: string, uri: URI): IFileCoverage => ({ + id, + uri, + testId: undefined, + statement: ICoverageCount.empty(), + }); } function serializeThingWithLocation(serialized: T): T & { location?: IRange | IPosition } { diff --git a/src/vs/workbench/contrib/testing/test/common/testCoverage.test.ts b/src/vs/workbench/contrib/testing/test/common/testCoverage.test.ts new file mode 100644 index 00000000000..bad192a7c02 --- /dev/null +++ b/src/vs/workbench/contrib/testing/test/common/testCoverage.test.ts @@ -0,0 +1,194 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { SinonSandbox, createSandbox } from 'sinon'; +import { Iterable } from 'vs/base/common/iterator'; +import { URI } from 'vs/base/common/uri'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { onObservableChange } from 'vs/workbench/contrib/testing/common/observableUtils'; +import { ICoverageAccessor, TestCoverage } from 'vs/workbench/contrib/testing/common/testCoverage'; +import { TestId } from 'vs/workbench/contrib/testing/common/testId'; +import { IFileCoverage } from 'vs/workbench/contrib/testing/common/testTypes'; + +suite('TestCoverage', () => { + let sandbox: SinonSandbox; + let coverageAccessor: ICoverageAccessor; + let testCoverage: TestCoverage; + + const ds = ensureNoDisposablesAreLeakedInTestSuite(); + + setup(() => { + sandbox = createSandbox(); + coverageAccessor = { + getCoverageDetails: sandbox.stub().resolves([]), + }; + testCoverage = new TestCoverage('taskId', { extUri: { ignorePathCasing: () => true } } as any, coverageAccessor); + }); + + teardown(() => { + sandbox.restore(); + }); + + function addTests() { + const raw1: IFileCoverage = { + id: '1', + uri: URI.file('/path/to/file'), + statement: { covered: 10, total: 20 }, + branch: { covered: 5, total: 10 }, + declaration: { covered: 2, total: 5 }, + }; + + testCoverage.append(raw1, undefined); + + const raw2: IFileCoverage = { + id: '1', + uri: URI.file('/path/to/file2'), + statement: { covered: 5, total: 10 }, + branch: { covered: 1, total: 5 }, + }; + + testCoverage.append(raw2, undefined); + + return { raw1, raw2 }; + } + + test('should look up file coverage', async () => { + const { raw1 } = addTests(); + + const fileCoverage = testCoverage.getUri(raw1.uri); + assert.equal(fileCoverage?.id, raw1.id); + assert.deepEqual(fileCoverage?.statement, raw1.statement); + assert.deepEqual(fileCoverage?.branch, raw1.branch); + assert.deepEqual(fileCoverage?.declaration, raw1.declaration); + assert.strictEqual(fileCoverage?.existsInExtHost, true); + + assert.strictEqual(testCoverage.getComputedForUri(raw1.uri), testCoverage.getUri(raw1.uri)); + assert.strictEqual(testCoverage.getComputedForUri(URI.file('/path/to/x')), undefined); + assert.strictEqual(testCoverage.getUri(URI.file('/path/to/x')), undefined); + }); + + test('should compute coverage for directories', async () => { + const { raw1 } = addTests(); + const dirCoverage = testCoverage.getComputedForUri(URI.file('/path/to')); + assert.deepEqual(dirCoverage?.statement, { covered: 15, total: 30 }); + assert.deepEqual(dirCoverage?.branch, { covered: 6, total: 15 }); + assert.deepEqual(dirCoverage?.declaration, raw1.declaration); + assert.strictEqual(dirCoverage?.existsInExtHost, false); + }); + + test('should incrementally diff updates to existing files', async () => { + addTests(); + + const raw3: IFileCoverage = { + id: '1', + uri: URI.file('/path/to/file'), + statement: { covered: 12, total: 24 }, + branch: { covered: 7, total: 10 }, + declaration: { covered: 2, total: 5 }, + }; + + testCoverage.append(raw3, undefined); + + const fileCoverage = testCoverage.getUri(raw3.uri); + assert.deepEqual(fileCoverage?.statement, raw3.statement); + assert.deepEqual(fileCoverage?.branch, raw3.branch); + assert.deepEqual(fileCoverage?.declaration, raw3.declaration); + + const dirCoverage = testCoverage.getComputedForUri(URI.file('/path/to')); + assert.deepEqual(dirCoverage?.statement, { covered: 17, total: 34 }); + assert.deepEqual(dirCoverage?.branch, { covered: 8, total: 15 }); + assert.deepEqual(dirCoverage?.declaration, raw3.declaration); + }); + + test('should emit changes', async () => { + const changes: string[][] = []; + ds.add(onObservableChange(testCoverage.didAddCoverage, value => + changes.push(value.map(v => v.value!.uri.toString())))); + + addTests(); + + assert.deepStrictEqual(changes, [ + [ + "file:///", + "file:///", + "file:///", + "file:///path", + "file:///path/to", + "file:///path/to/file", + ], + [ + "file:///", + "file:///", + "file:///", + "file:///path", + "file:///path/to", + "file:///path/to/file2", + ], + ]); + }); + + test('adds per-test data to files', async () => { + const { raw1 } = addTests(); + + const raw3: IFileCoverage = { + id: '1', + testId: TestId.fromString('my-test'), + uri: URI.file('/path/to/file'), + statement: { covered: 12, total: 24 }, + branch: { covered: 7, total: 10 }, + declaration: { covered: 2, total: 5 }, + }; + testCoverage.append(raw3, undefined); + + const fileCoverage = testCoverage.getUri(raw1.uri); + assert.strictEqual(fileCoverage?.perTestData?.size, 1); + + const perTestCoverage = Iterable.first(fileCoverage!.perTestData!.values()); + assert.deepStrictEqual(perTestCoverage?.statement, raw3.statement); + assert.deepStrictEqual(perTestCoverage?.branch, raw3.branch); + assert.deepStrictEqual(perTestCoverage?.declaration, raw3.declaration); + + // should be unchanged: + assert.deepEqual(fileCoverage?.statement, { covered: 10, total: 20 }); + assert.deepEqual(fileCoverage?.existsInExtHost, true); + const dirCoverage = testCoverage.getComputedForUri(URI.file('/path/to')); + assert.deepEqual(dirCoverage?.statement, { covered: 15, total: 30 }); + }); + + test('works if per-test data is added first', async () => { + const raw3: IFileCoverage = { + id: '1', + testId: TestId.fromString('my-test'), + uri: URI.file('/path/to/file'), + statement: { covered: 12, total: 24 }, + branch: { covered: 7, total: 10 }, + declaration: { covered: 2, total: 5 }, + }; + testCoverage.append(raw3, undefined); + + const fileCoverage = testCoverage.getUri(raw3.uri); + assert.deepEqual(fileCoverage?.existsInExtHost, false); + + addTests(); + + assert.strictEqual(fileCoverage?.perTestData?.size, 1); + const perTestCoverage = Iterable.first(fileCoverage!.perTestData!.values()); + assert.deepStrictEqual(perTestCoverage?.statement, raw3.statement); + assert.deepStrictEqual(perTestCoverage?.branch, raw3.branch); + assert.deepStrictEqual(perTestCoverage?.declaration, raw3.declaration); + + // should be the expected values: + assert.deepEqual(fileCoverage?.statement, { covered: 10, total: 20 }); + assert.deepEqual(fileCoverage?.existsInExtHost, true); + const dirCoverage = testCoverage.getComputedForUri(URI.file('/path/to')); + assert.deepEqual(dirCoverage?.statement, { covered: 15, total: 30 }); + }); +}); diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index ccc37fcf750..19988575941 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -9,6 +9,7 @@ export const allApiProposals = Object.freeze({ activeComment: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.activeComment.d.ts', aiRelatedInformation: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.aiRelatedInformation.d.ts', aiTextSearchProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.aiTextSearchProvider.d.ts', + attributableCoverage: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.attributableCoverage.d.ts', authGetSessions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authGetSessions.d.ts', authLearnMore: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authLearnMore.d.ts', authSession: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authSession.d.ts', diff --git a/src/vscode-dts/vscode.proposed.attributableCoverage.d.ts b/src/vscode-dts/vscode.proposed.attributableCoverage.d.ts new file mode 100644 index 00000000000..63000738c0f --- /dev/null +++ b/src/vscode-dts/vscode.proposed.attributableCoverage.d.ts @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + export class FileCoverage2 extends FileCoverage { + /** + * Test {@link TestItem} this file coverage is generated from. If undefined, + * the editor will assume the coverage is the overall summary coverage for + * the entire file. + * + * If per-test coverage is available, an extension should append multiple + * `FileCoverage` instances with this property set for each test item. It + * must also append a `FileCoverage` instance without this property set to + * represent the overall coverage of the file. + */ + testItem?: TestItem; + + constructor( + uri: Uri, + statementCoverage: TestCoverageCount, + branchCoverage?: TestCoverageCount, + declarationCoverage?: TestCoverageCount, + testItem?: TestItem, + ); + } +} From 380c60cfc772552736d6c08d859f79683d58889a Mon Sep 17 00:00:00 2001 From: David Dossett Date: Tue, 7 May 2024 16:49:27 -0700 Subject: [PATCH 033/357] Tweak notification widget styles --- src/vs/workbench/contrib/chat/browser/media/chat.css | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 55f4356b0af..13dd73ac9f1 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -413,10 +413,9 @@ .chat-notification-widget .chat-info-codicon, .chat-notification-widget .chat-error-codicon, .chat-notification-widget .chat-warning-codicon { - margin-left: 3px; display: flex; align-items: start; - gap: 6px; + gap: 8px; } .interactive-item-container .value .chat-notification-widget .rendered-markdown p { @@ -629,6 +628,10 @@ padding: 4px; } +.interactive-item-container .chat-notification-widget { + padding: 8px 12px; +} + .interactive-session .chat-used-context-list .monaco-list .monaco-list-row { border-radius: 2px; } From a11c57d9f6c0e84f5ed0fd066722d9b4e3113e36 Mon Sep 17 00:00:00 2001 From: David Dossett Date: Tue, 7 May 2024 16:57:17 -0700 Subject: [PATCH 034/357] Avoid the piano roll effect of alternating chat background colors --- src/vs/workbench/contrib/chat/browser/media/chat.css | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 55f4356b0af..dd72d0fd011 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -219,7 +219,6 @@ .interactive-request { border-bottom: 1px solid var(--vscode-chat-requestBorder); border-top: 1px solid var(--vscode-chat-requestBorder); - background-color: var(--vscode-chat-requestBackground); } .hc-black .interactive-request, From 0801d3cb1089d8d1bebc6548e0d43c00ae07018b Mon Sep 17 00:00:00 2001 From: David Dossett Date: Tue, 7 May 2024 17:02:13 -0700 Subject: [PATCH 035/357] Use loading icon instead of sync icon --- src/vs/workbench/contrib/chat/browser/chatListRenderer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index a05023440ee..9019394e226 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -890,7 +890,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer Date: Tue, 7 May 2024 17:08:03 -0700 Subject: [PATCH 036/357] Tweak progress step spacing --- src/vs/workbench/contrib/chat/browser/media/chat.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 55f4356b0af..5876b8eea1a 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -676,9 +676,9 @@ color: var(--vscode-descriptionForeground); font-size: 12px; display: flex; - gap: 5px; + gap: 8px; align-items: center; - margin-bottom: 4px; + margin-bottom: 6px; } .interactive-item-container .rendered-markdown.progress-step > p .codicon { From 60468916e48fe8293e7641f0fc4ada80d79117b3 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 8 May 2024 01:14:33 +0000 Subject: [PATCH 037/357] Implement chat confirmations (#212140) * Add API proposal for chat confirmations * Update * Fix up chatParticipantAdditions check * And copy warning content * Redundant * Implement chat confirmation widget * Implement sending chat confirmation data in the request * Styling * Fix button titles * Fix build --- .../workbench/api/common/extHost.api.impl.ts | 1 + .../api/common/extHostChatAgents2.ts | 12 +++- .../api/common/extHostTypeConverters.ts | 17 +++++- src/vs/workbench/api/common/extHostTypes.ts | 11 ++++ .../chat/browser/chatConfirmationWidget.ts | 58 +++++++++++++++++++ .../contrib/chat/browser/chatListRenderer.ts | 52 ++++++++++++++--- .../contrib/chat/browser/media/chat.css | 30 +++++++++- .../contrib/chat/common/chatAgents.ts | 2 + .../contrib/chat/common/chatModel.ts | 19 ++++-- .../contrib/chat/common/chatService.ts | 15 ++++- .../contrib/chat/common/chatServiceImpl.ts | 31 ++++++++-- .../contrib/chat/common/chatViewModel.ts | 4 +- ...ode.proposed.chatParticipantAdditions.d.ts | 40 ++++++++++++- 13 files changed, 264 insertions(+), 28 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/chatConfirmationWidget.ts diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index d0b5accb676..599fce994ee 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1724,6 +1724,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ChatResponseMarkdownWithVulnerabilitiesPart: extHostTypes.ChatResponseMarkdownWithVulnerabilitiesPart, ChatResponseCommandButtonPart: extHostTypes.ChatResponseCommandButtonPart, ChatResponseDetectedParticipantPart: extHostTypes.ChatResponseDetectedParticipantPart, + ChatResponseConfirmationPart: extHostTypes.ChatResponseConfirmationPart, ChatRequestTurn: extHostTypes.ChatRequestTurn, ChatResponseTurn: extHostTypes.ChatResponseTurn, ChatLocation: extHostTypes.ChatLocation, diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 9f3631f5a78..d981bf150d2 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -182,6 +182,15 @@ class ChatAgentResponseStream { _report(dto); return this; }, + confirmation(title, message, data) { + throwIfDone(this.confirmation); + checkProposedApiEnabled(that._extension, 'chatParticipantAdditions'); + + const part = new extHostTypes.ChatResponseConfirmationPart(title, message, data); + const dto = typeConvert.ChatResponseConfirmationPart.from(part); + _report(dto); + return this; + }, push(part) { throwIfDone(this.push); @@ -189,7 +198,8 @@ class ChatAgentResponseStream { part instanceof extHostTypes.ChatResponseTextEditPart || part instanceof extHostTypes.ChatResponseMarkdownWithVulnerabilitiesPart || part instanceof extHostTypes.ChatResponseDetectedParticipantPart || - part instanceof extHostTypes.ChatResponseWarningPart + part instanceof extHostTypes.ChatResponseWarningPart || + part instanceof extHostTypes.ChatResponseConfirmationPart ) { checkProposedApiEnabled(that._extension, 'chatParticipantAdditions'); } diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 6d3beb0e99a..4d7ec217589 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -40,7 +40,7 @@ import { DEFAULT_EDITOR_ASSOCIATION, SaveReason } from 'vs/workbench/common/edit import { IViewBadge } from 'vs/workbench/common/views'; import { ChatAgentLocation, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatRequestVariableEntry } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IChatAgentDetection, IChatAgentMarkdownContentWithVulnerability, IChatCommandButton, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgressMessage, IChatTextEdit, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatAgentDetection, IChatAgentMarkdownContentWithVulnerability, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgressMessage, IChatTextEdit, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from 'vs/workbench/contrib/chat/common/chatService'; import * as chatProvider from 'vs/workbench/contrib/chat/common/languageModels'; import { DebugTreeItemCollapsibleState, IDebugVisualizationTreeItem } from 'vs/workbench/contrib/debug/common/debug'; import * as notebooks from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -2297,6 +2297,17 @@ export namespace ChatResponseDetectedParticipantPart { } } +export namespace ChatResponseConfirmationPart { + export function from(part: vscode.ChatResponseConfirmationPart): Dto { + return { + kind: 'confirmation', + title: part.title, + message: part.message, + data: part.data + }; + } +} + export namespace ChatResponseFilesPart { export function from(part: vscode.ChatResponseFileTreePart): IChatTreeData { const { value, baseUri } = part; @@ -2452,7 +2463,7 @@ export namespace ChatResponseReferencePart { export namespace ChatResponsePart { - export function from(part: vscode.ChatResponsePart | vscode.ChatResponseTextEditPart | vscode.ChatResponseMarkdownWithVulnerabilitiesPart | vscode.ChatResponseDetectedParticipantPart | vscode.ChatResponseWarningPart, commandsConverter: CommandsConverter, commandDisposables: DisposableStore): extHostProtocol.IChatProgressDto { + export function from(part: vscode.ChatResponsePart | vscode.ChatResponseTextEditPart | vscode.ChatResponseMarkdownWithVulnerabilitiesPart | vscode.ChatResponseDetectedParticipantPart | vscode.ChatResponseWarningPart | vscode.ChatResponseConfirmationPart | vscode.ChatResponseWarningPart, commandsConverter: CommandsConverter, commandDisposables: DisposableStore): extHostProtocol.IChatProgressDto { if (part instanceof types.ChatResponseMarkdownPart) { return ChatResponseMarkdownPart.from(part); } else if (part instanceof types.ChatResponseAnchorPart) { @@ -2516,6 +2527,8 @@ export namespace ChatAgentRequest { enableCommandDetection: request.enableCommandDetection ?? true, variables: request.variables.variables.map(ChatAgentValueReference.to), location: ChatLocation.to(request.location), + acceptedConfirmationData: request.acceptedConfirmationData, + rejectedConfirmationData: request.rejectedConfirmationData }; } } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 220035f98f0..b0e4c2ce69e 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -4337,6 +4337,17 @@ export class ChatResponseDetectedParticipantPart { } } +export class ChatResponseConfirmationPart { + title: string; + message: string; + data: any; + constructor(title: string, message: string, data: any) { + this.title = title; + this.message = message; + this.data = data; + } +} + export class ChatResponseFileTreePart { value: vscode.ChatResponseFileTree[]; baseUri: vscode.Uri; diff --git a/src/vs/workbench/contrib/chat/browser/chatConfirmationWidget.ts b/src/vs/workbench/contrib/chat/browser/chatConfirmationWidget.ts new file mode 100644 index 00000000000..462cd0cb6ea --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatConfirmationWidget.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import { Button } from 'vs/base/browser/ui/button/button'; +import { Emitter, Event } from 'vs/base/common/event'; +import { MarkdownString } from 'vs/base/common/htmlContent'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles'; + +export interface IChatConfirmationButton { + label: string; + isSecondary?: boolean; + data: any; +} + +export class ChatConfirmationWidget extends Disposable { + private _onDidClick = this._register(new Emitter()); + get onDidClick(): Event { return this._onDidClick.event; } + + private _domNode: HTMLElement; + get domNode(): HTMLElement { + return this._domNode; + } + + constructor( + title: string, + message: string, + buttons: IChatConfirmationButton[], + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(); + + const elements = dom.h('.chat-confirmation-widget@root', [ + dom.h('.chat-confirmation-widget-title@title'), + dom.h('.chat-confirmation-widget-message@message'), + dom.h('.chat-confirmation-buttons-container@buttonsContainer'), + ]); + this._domNode = elements.root; + const renderer = this._register(this.instantiationService.createInstance(MarkdownRenderer, {})); + + const renderedTitle = this._register(renderer.render(new MarkdownString(title))); + elements.title.appendChild(renderedTitle.element); + + const renderedMessage = this._register(renderer.render(new MarkdownString(message))); + elements.message.appendChild(renderedMessage.element); + + buttons.forEach(buttonData => { + const button = new Button(elements.buttonsContainer, { ...defaultButtonStyles, secondary: buttonData.isSecondary }); + button.label = buttonData.label; + this._register(button.onDidClick(() => this._onDidClick.fire(buttonData))); + }); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 9019394e226..c7baf4327cb 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -33,11 +33,13 @@ import { equalsIgnoreCase } from 'vs/base/common/strings'; import { ThemeIcon } from 'vs/base/common/themables'; import { isUndefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; +import { generateUuid } from 'vs/base/common/uuid'; import { IMarkdownRenderResult, MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; import { Range } from 'vs/editor/common/core/range'; import { TextEdit } from 'vs/editor/common/languages'; import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel'; import { IModelService } from 'vs/editor/common/services/model'; +import { DefaultModelSHA1Computer } from 'vs/editor/common/services/modelService'; import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; import { localize } from 'vs/nls'; import { IMenuEntryActionViewItemOptions, MenuEntryActionViewItem, createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; @@ -59,6 +61,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; import { ChatTreeItem, GeneratingPhrase, IChatCodeBlockInfo, IChatFileTreeInfo } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatAgentHover } from 'vs/workbench/contrib/chat/browser/chatAgentHover'; +import { ChatConfirmationWidget } from 'vs/workbench/contrib/chat/browser/chatConfirmationWidget'; import { ChatFollowups } from 'vs/workbench/contrib/chat/browser/chatFollowups'; import { ChatMarkdownDecorationsRenderer } from 'vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer'; import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions'; @@ -67,18 +70,16 @@ import { ChatAgentLocation, IChatAgentMetadata, IChatAgentNameService } from 'vs import { CONTEXT_CHAT_RESPONSE_SUPPORT_ISSUE_REPORTING, CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_DETECTED_AGENT_COMMAND, CONTEXT_RESPONSE_FILTERED, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatProgressRenderableResponseContent, IChatTextEditGroup } from 'vs/workbench/contrib/chat/common/chatModel'; import { chatAgentLeader, chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { IChatCommandButton, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseProgressFileTreeData, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatCommandButton, IChatConfirmation, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseProgressFileTreeData, IChatSendRequestOptions, IChatService, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import { IChatProgressMessageRenderData, IChatRenderData, IChatResponseMarkdownRenderData, IChatResponseViewModel, IChatWelcomeMessageViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { IWordCountResult, getNWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; import { createFileIconThemableTreeContainerScope } from 'vs/workbench/contrib/files/browser/views/explorerView'; import { IFilesConfiguration } from 'vs/workbench/contrib/files/common/files'; +import { ITrustedDomainService } from 'vs/workbench/contrib/url/browser/trustedDomainService'; import { IMarkdownVulnerability, annotateSpecialMarkdownContent } from '../common/annotations'; import { CodeBlockModelCollection } from '../common/codeBlockModelCollection'; import { IChatListItemRendererOptions } from './chat'; -import { DefaultModelSHA1Computer } from 'vs/editor/common/services/modelService'; -import { generateUuid } from 'vs/base/common/uuid'; -import { ITrustedDomainService } from 'vs/workbench/contrib/url/browser/trustedDomainService'; const $ = dom.$; @@ -159,6 +160,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { + if (isResponseVM(element)) { + const prompt = `${e.label}: "${confirmation.title}"`; + const data: IChatSendRequestOptions = e.isSecondary ? + { rejectedConfirmationData: [e.data] } : + { acceptedConfirmationData: [e.data] }; + data.agentId = element.agent?.id; + this.chatService.sendRequest(element.sessionId, prompt, data); + } + })); + + return { + element: confirmationWidget.domNode, + dispose() { store.dispose(); } + }; + } + private renderTextEdit(element: ChatTreeItem, chatTextEdit: IChatTextEditGroup, templateData: IChatListItemTemplate): IMarkdownRenderResult | undefined { // TODO@jrieken move this into the CompareCodeBlock and properly say what kind of changes happen @@ -1617,6 +1647,10 @@ function isTextEditRenderData(item: IChatRenderData): item is IChatTextEditGroup return item && 'kind' in item && item.kind === 'textEditGroup'; } +function isConfirmationRenderData(item: IChatRenderData): item is IChatConfirmation { + return item && 'kind' in item && item.kind === 'confirmation'; +} + function isMarkdownRenderData(item: IChatRenderData): item is IChatResponseMarkdownRenderData { return item && 'renderedWordCount' in item; } diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 17c0049f1c6..5d543466f41 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -256,10 +256,13 @@ } .interactive-item-container .value .rendered-markdown p { - margin: 0 0 16px 0; line-height: 1.5em; } +.interactive-item-container .value > .rendered-markdown p { + margin: 0 0 16px 0; +} + .interactive-item-container .value .rendered-markdown ul { padding-inline-start: 24px; } @@ -317,7 +320,7 @@ min-height: 0; } -.interactive-item-container.interactive-item-compact .value .rendered-markdown p { +.interactive-item-container.interactive-item-compact .value > .rendered-markdown p { margin: 0 0 8px 0; } @@ -703,7 +706,8 @@ gap: 6px; } -.interactive-item-container .chat-command-button .monaco-button { +.interactive-item-container .chat-command-button .monaco-button, +.chat-confirmation-widget .chat-confirmation-buttons-container .monaco-button { text-align: left; width: initial; padding: 4px 8px; @@ -713,3 +717,23 @@ margin-left: 0; margin-top: 1px; } + +.chat-confirmation-widget { + border: 1px solid var(--vscode-chat-requestBorder); + border-radius: 6px; + margin-bottom: 16px; + padding: 10px 16px 12px; +} + +.chat-confirmation-widget .chat-confirmation-widget-title { + font-weight: 600; +} + +.chat-confirmation-widget .chat-confirmation-widget-title p { + margin: 0 0 4px 0; +} + +.chat-confirmation-widget .chat-confirmation-buttons-container { + display: flex; + gap: 8px; +} diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index 609a7c18dbf..277313e6d47 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -122,6 +122,8 @@ export interface IChatAgentRequest { enableCommandDetection?: boolean; variables: IChatRequestVariableData; location: ChatAgentLocation; + acceptedConfirmationData?: any[]; + rejectedConfirmationData?: any[]; } export interface IChatAgentResult { diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 7a7ffacd78e..3f456e6f2d7 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -20,7 +20,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ILogService } from 'vs/platform/log/common/log'; import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatRequestTextPart, IParsedChatRequest, getPromptText, reviveParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { IChatAgentMarkdownContentWithVulnerability, IChatCommandButton, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgress, IChatProgressMessage, IChatResponseProgressFileTreeData, IChatTextEdit, IChatTreeData, IChatUsedContext, IChatWarningMessage, InteractiveSessionVoteDirection, isIUsedContext } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatAgentMarkdownContentWithVulnerability, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgress, IChatProgressMessage, IChatResponseProgressFileTreeData, IChatTextEdit, IChatTreeData, IChatUsedContext, IChatWarningMessage, InteractiveSessionVoteDirection, isIUsedContext } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables'; export interface IChatRequestVariableEntry { @@ -63,7 +63,8 @@ export type IChatProgressResponseContent = | IChatProgressMessage | IChatCommandButton | IChatWarningMessage - | IChatTextEditGroup; + | IChatTextEditGroup + | IChatConfirmation; export type IChatProgressRenderableResponseContent = Exclude; @@ -207,7 +208,6 @@ export class Response implements IResponse { } this._updateRepr(quiet); } - } else { this._responseParts.push(progress); this._updateRepr(quiet); @@ -226,6 +226,8 @@ export class Response implements IResponse { return ''; } else if (part.kind === 'progressMessage') { return ''; + } else if (part.kind === 'confirmation') { + return `${part.title}\n${part.message}`; } else { return part.content.value; } @@ -778,7 +780,16 @@ export class ChatModel extends Disposable implements IChatModel { throw new Error('acceptResponseProgress: Adding progress to a completed response'); } - if (progress.kind === 'markdownContent' || progress.kind === 'treeData' || progress.kind === 'inlineReference' || progress.kind === 'markdownVuln' || progress.kind === 'progressMessage' || progress.kind === 'command' || progress.kind === 'textEdit' || progress.kind === 'warning') { + if (progress.kind === 'markdownContent' || + progress.kind === 'treeData' || + progress.kind === 'inlineReference' || + progress.kind === 'markdownVuln' || + progress.kind === 'progressMessage' || + progress.kind === 'command' || + progress.kind === 'textEdit' || + progress.kind === 'warning' || + progress.kind === 'confirmation' + ) { request.response.updateContent(progress, quiet); } else if (progress.kind === 'usedContext' || progress.kind === 'reference') { request.response.applyReference(progress); diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 26a25b167cb..c9a9be9a42e 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -133,6 +133,13 @@ export interface IChatTextEdit { kind: 'textEdit'; } +export interface IChatConfirmation { + title: string; + message: string; + data: any; + kind: 'confirmation'; +} + export type IChatProgress = | IChatMarkdownContent | IChatAgentMarkdownContentWithVulnerability @@ -144,7 +151,8 @@ export type IChatProgress = | IChatProgressMessage | IChatCommandButton | IChatWarningMessage - | IChatTextEdit; + | IChatTextEdit + | IChatConfirmation; export interface IChatFollowup { kind: 'reply'; @@ -268,6 +276,11 @@ export interface IChatSendRequestOptions { parserContext?: IChatParserContext; attempt?: number; noCommandDetection?: boolean; + acceptedConfirmationData?: any[]; + rejectedConfirmationData?: any[]; + + /** The target agent ID can be specified with this property instead of using @ in 'message' */ + agentId?: string; } export const IChatService = createDecorator('IChatService'); diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index 90f32ecbd10..5f22e62a370 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -25,7 +25,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ChatAgentLocation, IChatAgent, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatModel, ChatRequestModel, ChatWelcomeMessageModel, IChatModel, IChatRequestModel, IChatRequestVariableData, IChatRequestVariableEntry, IExportableChatData, 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 { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, IParsedChatRequest, chatAgentLeader, getPromptText } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; import { ChatCopyKind, IChatCompleteResponse, IChatDetail, IChatFollowup, IChatProgress, IChatSendRequestData, IChatSendRequestOptions, IChatService, IChatTransferredSessionData, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; @@ -116,6 +116,11 @@ type ChatTerminalClassification = { comment: 'Provides insight into the usage of Chat features.'; }; +interface IRequestConfirmationData { + acceptedConfirmationData?: any[]; + rejectedConfirmationData?: any[]; +} + const maxPersistedSessions = 25; export class ChatService extends Disposable implements IChatService { @@ -460,18 +465,33 @@ export class ChatService extends Disposable implements IChatService { const implicitVariablesEnabled = options?.implicitVariablesEnabled ?? false; const defaultAgent = this.chatAgentService.getDefaultAgent(location)!; - const parsedRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionId, request, location, options?.parserContext); + const parsedRequest = this.parseChatRequest(sessionId, request, location, options); 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); // This method is only returning whether the request was accepted - don't block on the actual request return { - responseCompletePromise: this._sendRequestAsync(model, sessionId, parsedRequest, attempt, !options?.noCommandDetection, implicitVariablesEnabled, defaultAgent, location), + responseCompletePromise: this._sendRequestAsync(model, sessionId, parsedRequest, attempt, !options?.noCommandDetection, implicitVariablesEnabled, defaultAgent, location, options), agent, slashCommand: agentSlashCommandPart?.command, }; } + private parseChatRequest(sessionId: string, request: string, location: ChatAgentLocation, options: IChatSendRequestOptions | undefined): IParsedChatRequest { + let parserContext = options?.parserContext; + if (options?.agentId) { + const agent = this.chatAgentService.getAgent(options.agentId); + if (!agent) { + throw new Error(`Unknown agent: ${options.agentId}`); + } + parserContext = { selectedAgent: agent }; + request = `${chatAgentLeader}${agent.name} ${request}`; + } + + const parsedRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionId, request, location, parserContext); + return parsedRequest; + } + private refreshFollowupsCancellationToken(sessionId: string): CancellationToken { this._sessionFollowupCancelTokens.get(sessionId)?.cancel(); const newTokenSource = new CancellationTokenSource(); @@ -480,7 +500,7 @@ export class ChatService extends Disposable implements IChatService { return newTokenSource.token; } - private async _sendRequestAsync(model: ChatModel, sessionId: string, parsedRequest: IParsedChatRequest, attempt: number, enableCommandDetection: boolean, implicitVariablesEnabled: boolean, defaultAgent: IChatAgent, location: ChatAgentLocation): Promise { + private async _sendRequestAsync(model: ChatModel, sessionId: string, parsedRequest: IParsedChatRequest, attempt: number, enableCommandDetection: boolean, implicitVariablesEnabled: boolean, defaultAgent: IChatAgent, location: ChatAgentLocation, confirmData?: IRequestConfirmationData): Promise { const followupsCancelToken = this.refreshFollowupsCancellationToken(sessionId); let request: ChatRequestModel; const agentPart = 'kind' in parsedRequest ? undefined : parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart); @@ -563,7 +583,8 @@ export class ChatService extends Disposable implements IChatService { variables: updatedVariableData, enableCommandDetection, attempt, - location + location, + ...confirmData }; const agentResult = await this.chatAgentService.invokeAgent(agent.id, requestProps, progressCallback, history, token); diff --git a/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/chatViewModel.ts index 8a39c4d2e63..429d32cbc2c 100644 --- a/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatViewModel.ts @@ -14,7 +14,7 @@ import { annotateVulnerabilitiesInText } from 'vs/workbench/contrib/chat/common/ import { IChatAgentCommand, IChatAgentData, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatModelInitState, IChatModel, IChatRequestModel, IChatResponseModel, IChatTextEditGroup, IChatWelcomeMessageContent, IResponse } from 'vs/workbench/contrib/chat/common/chatModel'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { IChatCommandButton, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseErrorDetails, IChatResponseProgressFileTreeData, IChatUsedContext, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatCommandButton, IChatConfirmation, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseErrorDetails, IChatResponseProgressFileTreeData, IChatUsedContext, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; import { CodeBlockModelCollection } from './codeBlockModelCollection'; import { IMarkdownString } from 'vs/base/common/htmlContent'; @@ -95,7 +95,7 @@ export interface IChatProgressMessageRenderData { isLast: boolean; } -export type IChatRenderData = IChatResponseProgressFileTreeData | IChatResponseMarkdownRenderData | IChatProgressMessageRenderData | IChatCommandButton | IChatTextEditGroup; +export type IChatRenderData = IChatResponseProgressFileTreeData | IChatResponseMarkdownRenderData | IChatProgressMessageRenderData | IChatCommandButton | IChatTextEditGroup | IChatConfirmation; export interface IChatResponseRenderData { renderedParts: IChatRenderData[]; } diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index f8914d33b94..6ff2a8a71cf 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -102,6 +102,15 @@ declare module 'vscode' { constructor(uri: Uri, edits: TextEdit | TextEdit[]); } + export class ChatResponseConfirmationPart { + title: string; + message: string; + data: any; + constructor(title: string, message: string, data: any); + } + + export type ExtendedChatResponsePart = ChatResponsePart | ChatResponseTextEditPart | ChatResponseDetectedParticipantPart | ChatResponseConfirmationPart; + export class ChatResponseWarningPart { value: MarkdownString; constructor(value: string | MarkdownString); @@ -111,7 +120,19 @@ declare module 'vscode' { textEdit(target: Uri, edits: TextEdit | TextEdit[]): ChatResponseStream; markdownWithVulnerabilities(value: string | MarkdownString, vulnerabilities: ChatVulnerability[]): ChatResponseStream; detectedParticipant(participant: string, command?: ChatCommand): ChatResponseStream; - push(part: ChatResponsePart | ChatResponseTextEditPart | ChatResponseDetectedParticipantPart | ChatResponseWarningPart): ChatResponseStream; + + /** + * Show an inline message in the chat view asking the user to confirm an action. + * Multiple confirmations may be shown per response. The UI might show "Accept All" / "Reject All" actions. + * @param title The title of the confirmation entry + * @param message An extra message to display to the user + * @param data An arbitrary JSON-stringifiable object that will be included in the ChatRequest when + * the confirmation is accepted or rejected + * TODO@API should this be MarkdownString? + * TODO@API should actually be a more generic function that takes an array of buttons + */ + confirmation(title: string, message: string, data: any): ChatResponseStream; + /** * Push a warning to this stream. Short-hand for * `push(new ChatResponseWarningPart(message))`. @@ -121,6 +142,23 @@ declare module 'vscode' { */ warning(message: string | MarkdownString): ChatResponseStream; + push(part: ExtendedChatResponsePart): ChatResponseStream; + } + + /** + * Does this piggy-back on the existing ChatRequest, or is it a different type of request entirely? + * Does it show up in history? + */ + export interface ChatRequest { + /** + * The `data` for any confirmations that were accepted + */ + acceptedConfirmationData?: any[]; + + /** + * The `data` for any confirmations that were rejected + */ + rejectedConfirmationData?: any[]; } // TODO@API fit this into the stream From df41a938ef73307844e58b8ad430a013f46d1514 Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Tue, 7 May 2024 22:33:37 -0700 Subject: [PATCH 038/357] Remove redundant `info-needed` commands (#212225) Remove redundant info-needed related commands --- .github/commands.json | 81 ------------------------------------------- 1 file changed, 81 deletions(-) diff --git a/.github/commands.json b/.github/commands.json index 7b04c7475d7..df9fc791fd5 100644 --- a/.github/commands.json +++ b/.github/commands.json @@ -156,37 +156,6 @@ "addLabel": "confirmation-pending", "removeLabel": "confirmed" }, - { - "type": "comment", - "name": "needsMoreInfo", - "allowUsers": [ - "cleidigh", - "usernamehw", - "gjsjohnmurray", - "IllusionMH" - ], - "action": "updateLabels", - "addLabel": "~info-needed" - }, - { - "type": "comment", - "name": "needsPerfInfo", - "allowUsers": [ - "cleidigh", - "usernamehw", - "gjsjohnmurray", - "IllusionMH" - ], - "addLabel": "info-needed", - "comment": "Thanks for creating this issue regarding performance! Please follow this guide to help us diagnose performance issues: https://github.com/microsoft/vscode/wiki/Performance-Issues \n\nHappy Coding!" - }, - { - "type": "comment", - "name": "jsDebugLogs", - "action": "updateLabels", - "addLabel": "info-needed", - "comment": "Please collect trace logs using the following instructions:\n\n> If you're able to, add `\"trace\": true` to your `launch.json` and reproduce the issue. The location of the log file on your disk will be written to the Debug Console. Share that with us.\n>\n> ⚠️ This log file will not contain source code, but will contain file paths. You can drop it into https://microsoft.github.io/vscode-pwa-analyzer/index.html to see what it contains. If you'd rather not share the log publicly, you can email it to connor@xbox.com" - }, { "type": "comment", "name": "closedWith", @@ -200,30 +169,6 @@ "reason": "completed", "addLabel": "unreleased" }, - { - "type": "label", - "name": "~info-needed", - "action": "updateLabels", - "addLabel": "info-needed", - "removeLabel": "~info-needed", - "comment": "Thanks for creating this issue! We figured it's missing some basic information or in some other way doesn't follow our [issue reporting guidelines](https://aka.ms/vscodeissuereporting). Please take the time to review these and update the issue.\n\nHappy Coding!" - }, - { - "type": "label", - "name": "~version-info-needed", - "action": "updateLabels", - "addLabel": "info-needed", - "removeLabel": "~version-info-needed", - "comment": "Thanks for creating this issue! We figured it's missing some basic information, such as a version number, or in some other way doesn't follow our [issue reporting guidelines](https://aka.ms/vscodeissuereporting). Please take the time to review these and update the issue.\n\nHappy Coding!" - }, - { - "type": "label", - "name": "~confirmation-needed", - "action": "updateLabels", - "addLabel": "info-needed", - "removeLabel": "~confirmation-needed", - "comment": "Please diagnose the root cause of the issue by running the command `F1 > Help: Troubleshoot Issue` and following the instructions. Once you have done that, please update the issue with the results.\n\nHappy Coding!" - }, { "type": "comment", "name": "a11ymas", @@ -469,32 +414,6 @@ "addLabel": "*caused-by-extension", "comment": "It looks like this is caused by the Copilot extension. Please file the issue in the [Copilot Discussion Forum](https://github.com/community/community/discussions/categories/copilot). Make sure to check their issue reporting template and provide them relevant information such as the extension version you're using. See also our [issue reporting guidelines](https://aka.ms/vscodeissuereporting) for more information.\n\nHappy Coding!" }, - { - "type": "comment", - "name": "gifPlease", - "allowUsers": [ - "cleidigh", - "usernamehw", - "gjsjohnmurray", - "IllusionMH" - ], - "action": "comment", - "addLabel": "info-needed", - "comment": "Thanks for reporting this issue! Unfortunately, it's hard for us to understand what issue you're seeing. Please help us out by providing a screen recording showing exactly what isn't working as expected. While we can work with most standard formats, `.gif` files are preferred as they are displayed inline on GitHub. You may find https://gifcap.dev helpful as a browser-based gif recording tool.\n\nIf the issue depends on keyboard input, you can help us by enabling screencast mode for the recording (`Developer: Toggle Screencast Mode` in the command palette). Lastly, please attach this file via the GitHub web interface as emailed responses will strip files out from the issue.\n\nHappy coding!" - }, - { - "type": "comment", - "name": "confirmPlease", - "allowUsers": [ - "cleidigh", - "usernamehw", - "gjsjohnmurray", - "IllusionMH" - ], - "action": "comment", - "addLabel": "info-needed", - "comment": "Please diagnose the root cause of the issue by running the command `F1 > Help: Troubleshoot Issue` and following the instructions. Once you have done that, please update the issue with the results.\n\nHappy Coding!" - }, { "__comment__": "Allows folks on the team to label issues by commenting: `\\label My-Label` ", "type": "comment", From c06c555b481aaac4afd51d6fc7691d7658949651 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 8 May 2024 10:09:10 +0200 Subject: [PATCH 039/357] Add more extension install error codes (#212237) * Revert "Revert f8c7fec0c29c1f6d2fd65c3b2c779d362fe61d0b (#212203)" This reverts commit 2a57bf60e225bd5b22ab4ee219d09b83f3b901d2. * delete VSIX only when needed --- .../abstractExtensionManagementService.ts | 90 ++++---- .../common/extensionGalleryService.ts | 48 ++-- .../common/extensionManagement.ts | 36 +-- .../node/extensionDownloader.ts | 33 ++- .../node/extensionManagementService.ts | 213 ++++++++++-------- 5 files changed, 244 insertions(+), 176 deletions(-) diff --git a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts index 55a1e34ba60..95438e4a764 100644 --- a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts +++ b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts @@ -17,7 +17,7 @@ import { ExtensionManagementError, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementParticipant, IGalleryExtension, ILocalExtension, InstallOperation, IExtensionsControlManifest, StatisticType, isTargetPlatformCompatible, TargetPlatformToString, ExtensionManagementErrorCode, InstallOptions, UninstallOptions, Metadata, InstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, IExtensionManagementService, InstallExtensionInfo, EXTENSION_INSTALL_DEP_PACK_CONTEXT, ExtensionGalleryError, - IProductVersion + IProductVersion, ExtensionGalleryErrorCode } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions, ExtensionKey, getGalleryExtensionId, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionType, IExtensionManifest, isApplicationScopedExtension, TargetPlatform } from 'vs/platform/extensions/common/extensions'; @@ -290,26 +290,10 @@ export abstract class AbstractExtensionManagementService extends Disposable impl // Install extensions in parallel and wait until all extensions are installed / failed await this.joinAllSettled([...installingExtensionsMap.entries()].map(async ([key, { task }]) => { const startTime = new Date().getTime(); + let local: ILocalExtension; try { - const local = await task.run(); - await this.joinAllSettled(this.participants.map(participant => participant.postInstall(local, task.source, task.options, CancellationToken.None))); - if (!URI.isUri(task.source)) { - const isUpdate = task.operation === InstallOperation.Update; - const durationSinceUpdate = isUpdate ? undefined : (new Date().getTime() - task.source.lastUpdated) / 1000; - reportTelemetry(this.telemetryService, isUpdate ? 'extensionGallery:update' : 'extensionGallery:install', { - extensionData: getGalleryExtensionTelemetryData(task.source), - verificationStatus: task.verificationStatus, - duration: new Date().getTime() - startTime, - durationSinceUpdate - }); - // In web, report extension install statistics explicitly. In Desktop, statistics are automatically updated while downloading the VSIX. - if (isWeb && task.operation !== InstallOperation.Update) { - try { - await this.galleryService.reportStatistic(local.manifest.publisher, local.manifest.name, local.manifest.version, StatisticType.Install); - } catch (error) { /* ignore */ } - } - } - installExtensionResultsMap.set(key, { local, identifier: task.identifier, operation: task.operation, source: task.source, context: task.options.context, profileLocation: task.profileLocation, applicationScoped: local.isApplicationScoped }); + local = await task.run(); + await this.joinAllSettled(this.participants.map(participant => participant.postInstall(local, task.source, task.options, CancellationToken.None)), ExtensionManagementErrorCode.PostInstall); } catch (e) { const error = toExtensionManagementError(e); if (!URI.isUri(task.source)) { @@ -319,6 +303,23 @@ export abstract class AbstractExtensionManagementService extends Disposable impl this.logService.error('Error while installing the extension', task.identifier.id, getErrorMessage(error)); throw error; } + if (!URI.isUri(task.source)) { + const isUpdate = task.operation === InstallOperation.Update; + const durationSinceUpdate = isUpdate ? undefined : (new Date().getTime() - task.source.lastUpdated) / 1000; + reportTelemetry(this.telemetryService, isUpdate ? 'extensionGallery:update' : 'extensionGallery:install', { + extensionData: getGalleryExtensionTelemetryData(task.source), + verificationStatus: task.verificationStatus, + duration: new Date().getTime() - startTime, + durationSinceUpdate + }); + // In web, report extension install statistics explicitly. In Desktop, statistics are automatically updated while downloading the VSIX. + if (isWeb && task.operation !== InstallOperation.Update) { + try { + await this.galleryService.reportStatistic(local.manifest.publisher, local.manifest.name, local.manifest.version, StatisticType.Install); + } catch (error) { /* ignore */ } + } + } + installExtensionResultsMap.set(key, { local, identifier: task.identifier, operation: task.operation, source: task.source, context: task.options.context, profileLocation: task.profileLocation, applicationScoped: local.isApplicationScoped }); })); if (alreadyRequestedInstallations.length) { @@ -428,36 +429,35 @@ export abstract class AbstractExtensionManagementService extends Disposable impl return true; } - private async joinAllSettled(promises: Promise[]): Promise { + private async joinAllSettled(promises: Promise[], errorCode?: ExtensionManagementErrorCode): Promise { const results: T[] = []; - const errors: any[] = []; + const errors: ExtensionManagementError[] = []; const promiseResults = await Promise.allSettled(promises); for (const r of promiseResults) { if (r.status === 'fulfilled') { results.push(r.value); } else { - errors.push(r.reason); + errors.push(toExtensionManagementError(r.reason, errorCode)); } } + if (!errors.length) { + return results; + } + // Throw if there are errors - if (errors.length) { - if (errors.length === 1) { - throw errors[0]; - } - - let error = new ExtensionManagementError('', ExtensionManagementErrorCode.Unknown); - for (const current of errors) { - const code = current instanceof ExtensionManagementError ? current.code : ExtensionManagementErrorCode.Unknown; - error = new ExtensionManagementError( - current.message ? `${current.message}, ${error.message}` : error.message, - code !== ExtensionManagementErrorCode.Unknown && code !== ExtensionManagementErrorCode.Internal ? code : error.code - ); - } - throw error; + if (errors.length === 1) { + throw errors[0]; } - return results; + let error = new ExtensionManagementError('', ExtensionManagementErrorCode.Unknown); + for (const current of errors) { + error = new ExtensionManagementError( + error.message ? `${error.message}, ${current.message}` : current.message, + current.code !== ExtensionManagementErrorCode.Unknown && current.code !== ExtensionManagementErrorCode.Internal ? current.code : error.code + ); + } + throw error; } private async getAllDepsAndPackExtensions(extensionIdentifier: IExtensionIdentifier, manifest: IExtensionManifest, getOnlyNewlyAddedFromExtensionPack: boolean, installPreRelease: boolean, profile: URI | undefined, productVersion: IProductVersion): Promise<{ gallery: IGalleryExtension; manifest: IExtensionManifest }[]> { @@ -787,18 +787,18 @@ export abstract class AbstractExtensionManagementService extends Disposable impl protected abstract copyExtension(extension: ILocalExtension, fromProfileLocation: URI, toProfileLocation: URI, metadata?: Partial): Promise; } -export function toExtensionManagementError(error: Error): ExtensionManagementError { +export function toExtensionManagementError(error: Error, code?: ExtensionManagementErrorCode): ExtensionManagementError { if (error instanceof ExtensionManagementError) { return error; } + let extensionManagementError: ExtensionManagementError; if (error instanceof ExtensionGalleryError) { - const e = new ExtensionManagementError(error.message, ExtensionManagementErrorCode.Gallery); - e.stack = error.stack; - return e; + extensionManagementError = new ExtensionManagementError(error.message, error.code === ExtensionGalleryErrorCode.DownloadFailedWriting ? ExtensionManagementErrorCode.DownloadFailedWriting : ExtensionManagementErrorCode.Gallery); + } else { + extensionManagementError = new ExtensionManagementError(error.message, isCancellationError(error) ? ExtensionManagementErrorCode.Cancelled : (code ?? ExtensionManagementErrorCode.Internal)); } - const e = new ExtensionManagementError(error.message, isCancellationError(error) ? ExtensionManagementErrorCode.Cancelled : ExtensionManagementErrorCode.Internal); - e.stack = error.stack; - return e; + extensionManagementError.stack = error.stack; + return extensionManagementError; } function reportTelemetry(telemetryService: ITelemetryService, eventName: string, { extensionData, verificationStatus, duration, error, durationSinceUpdate }: { extensionData: any; verificationStatus?: ExtensionVerificationStatus; duration?: number; durationSinceUpdate?: number; error?: ExtensionManagementError | ExtensionGalleryError }): void { diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index ebdb1bb50b8..cc587b5770e 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -1029,16 +1029,6 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi this.logService.trace('ExtensionGalleryService#download', extension.identifier.id); const data = getGalleryExtensionTelemetryData(extension); const startTime = new Date().getTime(); - /* __GDPR__ - "galleryService:downloadVSIX" : { - "owner": "sandy081", - "duration": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "${include}": [ - "${GalleryExtensionTelemetryData}" - ] - } - */ - const log = (duration: number) => this.telemetryService.publicLog('galleryService:downloadVSIX', { ...data, duration }); const operationParam = operation === InstallOperation.Install ? 'install' : operation === InstallOperation.Update ? 'update' : ''; const downloadAsset = operationParam ? { @@ -1048,8 +1038,29 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi const headers: IHeaders | undefined = extension.queryContext?.[ACTIVITY_HEADER_NAME] ? { [ACTIVITY_HEADER_NAME]: extension.queryContext[ACTIVITY_HEADER_NAME] } : undefined; const context = await this.getAsset(extension.identifier.id, downloadAsset, AssetType.VSIX, headers ? { headers } : undefined); - await this.fileService.writeFile(location, context.stream); - log(new Date().getTime() - startTime); + + try { + await this.fileService.writeFile(location, context.stream); + } catch (error) { + try { + await this.fileService.del(location); + } catch (e) { + /* ignore */ + this.logService.warn(`Error while deleting the file ${location.toString()}`, getErrorMessage(e)); + } + throw new ExtensionGalleryError(getErrorMessage(error), ExtensionGalleryErrorCode.DownloadFailedWriting); + } + + /* __GDPR__ + "galleryService:downloadVSIX" : { + "owner": "sandy081", + "duration": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "${include}": [ + "${GalleryExtensionTelemetryData}" + ] + } + */ + this.telemetryService.publicLog('galleryService:downloadVSIX', { ...data, duration: new Date().getTime() - startTime }); } async downloadSignatureArchive(extension: IGalleryExtension, location: URI): Promise { @@ -1060,7 +1071,18 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi this.logService.trace('ExtensionGalleryService#downloadSignatureArchive', extension.identifier.id); const context = await this.getAsset(extension.identifier.id, extension.assets.signature, AssetType.Signature); - await this.fileService.writeFile(location, context.stream); + try { + await this.fileService.writeFile(location, context.stream); + } catch (error) { + try { + await this.fileService.del(location); + } catch (e) { + /* ignore */ + this.logService.warn(`Error while deleting the file ${location.toString()}`, getErrorMessage(e)); + } + throw new ExtensionGalleryError(getErrorMessage(error), ExtensionGalleryErrorCode.DownloadFailedWriting); + } + } async getReadme(extension: IGalleryExtension, token: CancellationToken): Promise { diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index d69233901b8..fa9e1b0983d 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -407,7 +407,21 @@ export interface DidUninstallExtensionEvent { readonly workspaceScoped?: boolean; } -export enum ExtensionManagementErrorCode { +export const enum ExtensionGalleryErrorCode { + Timeout = 'Timeout', + Cancelled = 'Cancelled', + Failed = 'Failed', + DownloadFailedWriting = 'DownloadFailedWriting', +} + +export class ExtensionGalleryError extends Error { + constructor(message: string, readonly code: ExtensionGalleryErrorCode) { + super(message); + this.name = code; + } +} + +export const enum ExtensionManagementErrorCode { Unsupported = 'Unsupported', Deprecated = 'Deprecated', Malicious = 'Malicious', @@ -417,11 +431,18 @@ export enum ExtensionManagementErrorCode { Invalid = 'Invalid', Download = 'Download', DownloadSignature = 'DownloadSignature', + DownloadFailedWriting = ExtensionGalleryErrorCode.DownloadFailedWriting, UpdateMetadata = 'UpdateMetadata', Extract = 'Extract', Scanning = 'Scanning', + ScanningExtension = 'ScanningExtension', + ReadUninstalled = 'ReadUninstalled', + UnsetUninstalled = 'UnsetUninstalled', Delete = 'Delete', Rename = 'Rename', + IntializeDefaultProfile = 'IntializeDefaultProfile', + AddToProfile = 'AddToProfile', + PostInstall = 'PostInstall', CorruptZip = 'CorruptZip', IncompleteZip = 'IncompleteZip', Signature = 'Signature', @@ -439,19 +460,6 @@ export class ExtensionManagementError extends Error { } } -export enum ExtensionGalleryErrorCode { - Timeout = 'Timeout', - Cancelled = 'Cancelled', - Failed = 'Failed' -} - -export class ExtensionGalleryError extends Error { - constructor(message: string, readonly code: ExtensionGalleryErrorCode) { - super(message); - this.name = code; - } -} - export type InstallOptions = { isBuiltin?: boolean; isWorkspaceScoped?: boolean; diff --git a/src/vs/platform/extensionManagement/node/extensionDownloader.ts b/src/vs/platform/extensionManagement/node/extensionDownloader.ts index 85fb4668e79..0a96fee799d 100644 --- a/src/vs/platform/extensionManagement/node/extensionDownloader.ts +++ b/src/vs/platform/extensionManagement/node/extensionDownloader.ts @@ -16,7 +16,7 @@ import { Promises as FSPromises } from 'vs/base/node/pfs'; import { CorruptZipMessage } from 'vs/base/node/zip'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; -import { ExtensionVerificationStatus } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService'; +import { ExtensionVerificationStatus, toExtensionManagementError } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService'; import { ExtensionManagementError, ExtensionManagementErrorCode, IExtensionGalleryService, IGalleryExtension, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionKey, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionSignatureVerificationError, ExtensionSignatureVerificationCode, IExtensionSignatureVerificationService } from 'vs/platform/extensionManagement/node/extensionSignatureVerificationService'; @@ -52,21 +52,33 @@ export class ExtensionsDownloader extends Disposable { try { await this.downloadFile(extension, location, location => this.extensionGalleryService.download(extension, location, operation)); } catch (error) { - throw new ExtensionManagementError(error.message, ExtensionManagementErrorCode.Download); + throw toExtensionManagementError(error, ExtensionManagementErrorCode.Download); } let verificationStatus: ExtensionVerificationStatus = false; if (verifySignature && this.shouldVerifySignature(extension)) { - const signatureArchiveLocation = await this.downloadSignatureArchive(extension); + + let signatureArchiveLocation; + try { + signatureArchiveLocation = await this.downloadSignatureArchive(extension); + } catch (error) { + try { + // Delete the downloaded VSIX if signature archive download fails + await this.delete(location); + } catch (error) { + this.logService.error(error); + } + throw error; + } + try { verificationStatus = await this.extensionSignatureVerificationService.verify(extension.identifier.id, location.fsPath, signatureArchiveLocation.fsPath); } catch (error) { - const sigError = error as ExtensionSignatureVerificationError; - verificationStatus = sigError.code; + verificationStatus = (error as ExtensionSignatureVerificationError).code; if (verificationStatus === ExtensionSignatureVerificationCode.PackageIsInvalidZip || verificationStatus === ExtensionSignatureVerificationCode.SignatureArchiveIsInvalidZip) { try { - // Delete the downloaded vsix before throwing the error + // Delete the downloaded vsix if VSIX or signature archive is invalid await this.delete(location); } catch (error) { this.logService.error(error); @@ -103,7 +115,7 @@ export class ExtensionsDownloader extends Disposable { try { await this.downloadFile(extension, location, location => this.extensionGalleryService.downloadSignatureArchive(extension, location)); } catch (error) { - throw new ExtensionManagementError(error.message, ExtensionManagementErrorCode.DownloadSignature); + throw toExtensionManagementError(error, ExtensionManagementErrorCode.DownloadSignature); } return location; } @@ -122,8 +134,13 @@ export class ExtensionsDownloader extends Disposable { // Download to temporary location first only if file does not exist const tempLocation = joinPath(this.extensionsDownloadDir, `.${generateUuid()}`); - if (!await this.fileService.exists(tempLocation)) { + try { await downloadFn(tempLocation); + } catch (error) { + try { + await this.fileService.del(tempLocation); + } catch (e) { /* ignore */ } + throw error; } try { diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 53630b739fe..122a6ed9b7b 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -451,20 +451,28 @@ export class ExtensionsScanner extends Disposable { } async scanExtensions(type: ExtensionType | null, profileLocation: URI, productVersion: IProductVersion): Promise { - const userScanOptions: ScanOptions = { includeInvalid: true, profileLocation, productVersion }; - let scannedExtensions: IScannedExtension[] = []; - if (type === null || type === ExtensionType.System) { - scannedExtensions.push(...await this.extensionsScannerService.scanAllExtensions({ includeInvalid: true }, userScanOptions, false)); - } else if (type === ExtensionType.User) { - scannedExtensions.push(...await this.extensionsScannerService.scanUserExtensions(userScanOptions)); + try { + const userScanOptions: ScanOptions = { includeInvalid: true, profileLocation, productVersion }; + let scannedExtensions: IScannedExtension[] = []; + if (type === null || type === ExtensionType.System) { + scannedExtensions.push(...await this.extensionsScannerService.scanAllExtensions({ includeInvalid: true }, userScanOptions, false)); + } else if (type === ExtensionType.User) { + scannedExtensions.push(...await this.extensionsScannerService.scanUserExtensions(userScanOptions)); + } + scannedExtensions = type !== null ? scannedExtensions.filter(r => r.type === type) : scannedExtensions; + return await Promise.all(scannedExtensions.map(extension => this.toLocalExtension(extension))); + } catch (error) { + throw toExtensionManagementError(error, ExtensionManagementErrorCode.Scanning); } - scannedExtensions = type !== null ? scannedExtensions.filter(r => r.type === type) : scannedExtensions; - return Promise.all(scannedExtensions.map(extension => this.toLocalExtension(extension))); } async scanAllUserExtensions(excludeOutdated: boolean): Promise { - const scannedExtensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: !excludeOutdated, includeInvalid: true }); - return Promise.all(scannedExtensions.map(extension => this.toLocalExtension(extension))); + try { + const scannedExtensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: !excludeOutdated, includeInvalid: true }); + return await Promise.all(scannedExtensions.map(extension => this.toLocalExtension(extension))); + } catch (error) { + throw toExtensionManagementError(error, ExtensionManagementErrorCode.Scanning); + } } async scanUserExtensionAtLocation(location: URI): Promise { @@ -496,7 +504,11 @@ export class ExtensionsScanner extends Disposable { } if (exists) { - await this.extensionsScannerService.updateMetadata(extensionLocation, metadata); + try { + await this.extensionsScannerService.updateMetadata(extensionLocation, metadata); + } catch (error) { + throw toExtensionManagementError(error, ExtensionManagementErrorCode.UpdateMetadata); + } } else { try { // Extract @@ -513,10 +525,14 @@ export class ExtensionsScanner extends Disposable { errorCode = ExtensionManagementErrorCode.IncompleteZip; } } - throw new ExtensionManagementError(e.message, errorCode); + throw toExtensionManagementError(e, errorCode); } - await this.extensionsScannerService.updateMetadata(tempLocation, metadata); + try { + await this.extensionsScannerService.updateMetadata(tempLocation, metadata); + } catch (error) { + throw toExtensionManagementError(error, ExtensionManagementErrorCode.UpdateMetadata); + } // Rename try { @@ -558,16 +574,24 @@ export class ExtensionsScanner extends Disposable { } async updateMetadata(local: ILocalExtension, metadata: Partial, profileLocation?: URI): Promise { - if (profileLocation) { - await this.extensionsProfileScannerService.updateMetadata([[local, metadata]], profileLocation); - } else { - await this.extensionsScannerService.updateMetadata(local.location, metadata); + try { + if (profileLocation) { + await this.extensionsProfileScannerService.updateMetadata([[local, metadata]], profileLocation); + } else { + await this.extensionsScannerService.updateMetadata(local.location, metadata); + } + } catch (error) { + throw toExtensionManagementError(error, ExtensionManagementErrorCode.UpdateMetadata); } return this.scanLocalExtension(local.location, local.type, profileLocation); } - getUninstalledExtensions(): Promise> { - return this.withUninstalledExtensions(); + async getUninstalledExtensions(): Promise> { + try { + return await this.withUninstalledExtensions(); + } catch (error) { + throw toExtensionManagementError(error, ExtensionManagementErrorCode.ReadUninstalled); + } } async setUninstalled(...extensions: IExtension[]): Promise { @@ -580,7 +604,11 @@ export class ExtensionsScanner extends Disposable { } async setInstalled(extensionKey: ExtensionKey): Promise { - await this.withUninstalledExtensions(uninstalled => delete uninstalled[extensionKey.toString()]); + try { + await this.withUninstalledExtensions(uninstalled => delete uninstalled[extensionKey.toString()]); + } catch (error) { + throw toExtensionManagementError(error, ExtensionManagementErrorCode.UnsetUninstalled); + } } async removeExtension(extension: ILocalExtension | IScannedExtension, type: string): Promise { @@ -630,7 +658,7 @@ export class ExtensionsScanner extends Disposable { this.logService.info(`Deleted ${type} extension from disk`, id, location.fsPath); } - private async withUninstalledExtensions(updateFn?: (uninstalled: IStringDictionary) => void): Promise> { + private withUninstalledExtensions(updateFn?: (uninstalled: IStringDictionary) => void): Promise> { return this.uninstalledFileLimiter.queue(async () => { let raw: string | undefined; try { @@ -666,24 +694,28 @@ export class ExtensionsScanner extends Disposable { try { await pfs.Promises.rename(extractPath, renamePath, 2 * 60 * 1000 /* Retry for 2 minutes */); } catch (error) { - throw new ExtensionManagementError(error.message || nls.localize('renameError', "Unknown error while renaming {0} to {1}", extractPath, renamePath), error.code || ExtensionManagementErrorCode.Rename); + throw toExtensionManagementError(error, ExtensionManagementErrorCode.Rename); } } private async scanLocalExtension(location: URI, type: ExtensionType, profileLocation?: URI): Promise { - if (profileLocation) { - const scannedExtensions = await this.extensionsScannerService.scanUserExtensions({ profileLocation }); - const scannedExtension = scannedExtensions.find(e => this.uriIdentityService.extUri.isEqual(e.location, location)); - if (scannedExtension) { - return this.toLocalExtension(scannedExtension); - } - } else { - const scannedExtension = await this.extensionsScannerService.scanExistingExtension(location, type, { includeInvalid: true }); - if (scannedExtension) { - return this.toLocalExtension(scannedExtension); + try { + if (profileLocation) { + const scannedExtensions = await this.extensionsScannerService.scanUserExtensions({ profileLocation }); + const scannedExtension = scannedExtensions.find(e => this.uriIdentityService.extUri.isEqual(e.location, location)); + if (scannedExtension) { + return await this.toLocalExtension(scannedExtension); + } + } else { + const scannedExtension = await this.extensionsScannerService.scanExistingExtension(location, type, { includeInvalid: true }); + if (scannedExtension) { + return await this.toLocalExtension(scannedExtension); + } } + throw new ExtensionManagementError(nls.localize('cannot read', "Cannot read the extension from {0}", location.path), ExtensionManagementErrorCode.ScanningExtension); + } catch (error) { + throw toExtensionManagementError(error, ExtensionManagementErrorCode.ScanningExtension); } - throw new Error(nls.localize('cannot read', "Cannot read the extension from {0}", location.path)); } private async toLocalExtension(extension: IScannedExtension): Promise { @@ -821,9 +853,17 @@ abstract class InstallExtensionTask extends AbstractExtensionTask { - const isUninstalled = await this.isUninstalled(extensionKey); - if (!isUninstalled) { + const uninstalled = await this.extensionsScanner.getUninstalledExtensions(); + if (!uninstalled[extensionKey.toString()]) { return undefined; } @@ -854,11 +894,6 @@ abstract class InstallExtensionTask extends AbstractExtensionTask ExtensionKey.create(i).equals(extensionKey)); } - private async isUninstalled(extensionId: ExtensionKey): Promise { - const uninstalled = await this.extensionsScanner.getUninstalledExtensions(); - return !!uninstalled[extensionId.toString()]; - } - protected abstract install(token: CancellationToken): Promise<[ILocalExtension, Metadata]>; } @@ -882,13 +917,7 @@ export class InstallGalleryExtensionTask extends InstallExtensionTask { } protected async install(token: CancellationToken): Promise<[ILocalExtension, Metadata]> { - let installed; - try { - installed = await this.extensionsScanner.scanExtensions(null, this.options.profileLocation, this.options.productVersion); - } catch (error) { - throw new ExtensionManagementError(error, ExtensionManagementErrorCode.Scanning); - } - + const installed = await this.extensionsScanner.scanExtensions(null, this.options.profileLocation, this.options.productVersion); const existingExtension = installed.find(i => areSameExtensions(i.identifier, this.gallery.identifier)); if (existingExtension) { this._operation = InstallOperation.Update; @@ -915,72 +944,64 @@ export class InstallGalleryExtensionTask extends InstallExtensionTask { }; if (existingExtension && existingExtension.type !== ExtensionType.System && existingExtension.manifest.version === this.gallery.version) { - try { - const local = await this.extensionsScanner.updateMetadata(existingExtension, metadata); - return [local, metadata]; - } catch (error) { - throw new ExtensionManagementError(getErrorMessage(error), ExtensionManagementErrorCode.UpdateMetadata); - } + const local = await this.extensionsScanner.updateMetadata(existingExtension, metadata); + return [local, metadata]; } - try { - return await this.downloadAndInstallExtension(metadata, token); - } catch (error) { - if (error instanceof ExtensionManagementError && (error.code === ExtensionManagementErrorCode.CorruptZip || error.code === ExtensionManagementErrorCode.IncompleteZip)) { - this.logService.info(`Downloaded VSIX is invalid. Trying to download and install again...`, this.gallery.identifier.id); - type RetryInstallingInvalidVSIXClassification = { - owner: 'sandy081'; - comment: 'Event reporting the retry of installing an invalid VSIX'; - extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Extension Id' }; - succeeded?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Success value' }; - }; - type RetryInstallingInvalidVSIXEvent = { - extensionId: string; - succeeded: boolean; - }; - try { - const result = await this.downloadAndInstallExtension(metadata, token); - this.telemetryService.publicLog2('extensiongallery:install:retry', { - extensionId: this.gallery.identifier.id, - succeeded: true - }); - return result; - } catch (error) { - this.telemetryService.publicLog2('extensiongallery:install:retry', { - extensionId: this.gallery.identifier.id, - succeeded: false - }); - throw error; - } - } else { - throw error; - } - } - } - - private async downloadAndInstallExtension(metadata: Metadata, token: CancellationToken): Promise<[ILocalExtension, Metadata]> { - const { location, verificationStatus } = await this.extensionsDownloader.download(this.gallery, this._operation, !this.options.donotVerifySignature); + const { verificationStatus, location } = await this.download(metadata, token); try { this._verificationStatus = verificationStatus; - this.validateManifest(location.fsPath); + await this.validateManifest(location.fsPath); const local = await this.extractExtension({ zipPath: location.fsPath, key: ExtensionKey.create(this.gallery), metadata }, false, token); return [local, metadata]; } catch (error) { try { await this.extensionsDownloader.delete(location); - } catch (error) { + } catch (e) { /* Ignore */ - this.logService.warn(`Error while deleting the downloaded file`, location.toString(), getErrorMessage(error)); + this.logService.warn(`Error while deleting the downloaded file`, location.toString(), getErrorMessage(e)); } throw error; } } + private async download(metadata: Metadata, token: CancellationToken): Promise<{ readonly location: URI; readonly verificationStatus: ExtensionVerificationStatus }> { + try { + return await this.extensionsDownloader.download(this.gallery, this._operation, !this.options.donotVerifySignature); + } catch (error) { + this.logService.info(`Failed downloading. Retry again...`, this.gallery.identifier.id); + type RetryDownloadingVSIXClassification = { + owner: 'sandy081'; + comment: 'Event reporting the retry of downloading the VSIX'; + extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Extension Id' }; + succeeded?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Success value' }; + }; + type RetryDownloadingVSIXEvent = { + extensionId: string; + succeeded: boolean; + }; + try { + const result = await this.download(metadata, token); + this.telemetryService.publicLog2('extensiongallery:download:retry', { + extensionId: this.gallery.identifier.id, + succeeded: true + }); + return result; + } catch (error) { + this.telemetryService.publicLog2('extensiongallery:download:retry', { + extensionId: this.gallery.identifier.id, + succeeded: false + }); + throw error; + } + } + } + protected async validateManifest(zipPath: string): Promise { try { await getManifest(zipPath); } catch (error) { - throw new ExtensionManagementError(error.message, ExtensionManagementErrorCode.Invalid); + throw toExtensionManagementError(error, ExtensionManagementErrorCode.Invalid); } } From e0065ce44183fbaa975b8da24f7c00ff94e3be53 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Wed, 8 May 2024 11:28:06 +0200 Subject: [PATCH 040/357] Cannot read properties of null (reading 'style') (#212250) Fixes #212231 --- src/vs/workbench/contrib/comments/browser/commentReply.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/comments/browser/commentReply.ts b/src/vs/workbench/contrib/comments/browser/commentReply.ts index 209b4a77ba5..3580e96799b 100644 --- a/src/vs/workbench/contrib/comments/browser/commentReply.ts +++ b/src/vs/workbench/contrib/comments/browser/commentReply.ts @@ -353,7 +353,10 @@ export class CommentReply extends Disposable { } private hideReplyArea() { - this.commentEditor.getDomNode()!.style.outline = ''; + const domNode = this.commentEditor.getDomNode(); + if (domNode) { + domNode.style.outline = ''; + } this.commentEditor.setValue(''); this._pendingComment = ''; this.form.classList.remove('expand'); From 0d55bb645f92bd6869b4953cf8ebd67703450f31 Mon Sep 17 00:00:00 2001 From: Johannes Date: Wed, 8 May 2024 11:44:47 +0200 Subject: [PATCH 041/357] add `LanguageModelChat` as explict object and add a select-function as the only way of getting to them --- .../api/browser/mainThreadLanguageModels.ts | 8 +- .../workbench/api/common/extHost.api.impl.ts | 23 +- .../workbench/api/common/extHost.protocol.ts | 4 +- .../api/common/extHostLanguageModels.ts | 155 +++++---- .../api/common/extHostTypeConverters.ts | 4 +- .../contrib/chat/common/languageModels.ts | 35 +- .../vscode.proposed.chatProvider.d.ts | 26 +- .../vscode.proposed.languageModels.d.ts | 323 ++++++------------ 8 files changed, 267 insertions(+), 311 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadLanguageModels.ts b/src/vs/workbench/api/browser/mainThreadLanguageModels.ts index fcce3b5456f..5c1f405b019 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageModels.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageModels.ts @@ -38,8 +38,8 @@ export class MainThreadLanguageModels implements MainThreadLanguageModelsShape { ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostChatProvider); - this._proxy.$updateLanguageModels({ added: coalesce(_chatProviderService.getLanguageModelIds().map(id => _chatProviderService.lookupLanguageModel(id))) }); - this._store.add(_chatProviderService.onDidChangeLanguageModels(this._proxy.$updateLanguageModels, this._proxy)); + this._proxy.$acceptChatModelMetadata({ added: coalesce(_chatProviderService.getLanguageModelIds().map(id => _chatProviderService.lookupLanguageModel(id))) }); + this._store.add(_chatProviderService.onDidChangeLanguageModels(this._proxy.$acceptChatModelMetadata, this._proxy)); } dispose(): void { @@ -78,6 +78,10 @@ export class MainThreadLanguageModels implements MainThreadLanguageModelsShape { this._providerRegistrations.deleteAndDispose(handle); } + $selectChatModels(selector: Partial): Promise { + return this._chatProviderService.selectLanguageModels(selector); + } + $whenLanguageModelChatRequestMade(identifier: string, extensionId: ExtensionIdentifier, participant?: string | undefined, tokenCount?: number | undefined): void { this._languageModelStatsService.update(identifier, extensionId, participant, tokenCount); } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 599fce994ee..2fc249f0d2a 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; import * as errors from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { combinedDisposable } from 'vs/base/common/lifecycle'; @@ -1428,29 +1428,14 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I // namespace: lm const lm: typeof vscode.lm = { - get languageModels() { + selectLanguageModels: (selector) => { checkProposedApiEnabled(extension, 'languageModels'); - return extHostLanguageModels.getLanguageModelIds(); + return extHostLanguageModels.selectLanguageModels(extension, selector); }, - onDidChangeLanguageModels: (listener, thisArgs?, disposables?) => { + onDidChangeChatModels: (listener, thisArgs?, disposables?) => { checkProposedApiEnabled(extension, 'languageModels'); return extHostLanguageModels.onDidChangeProviders(listener, thisArgs, disposables); }, - sendChatRequest(languageModel: string, messages: (vscode.LanguageModelChatMessage | vscode.LanguageModelChatMessage2)[], options?: vscode.LanguageModelChatRequestOptions, token?: vscode.CancellationToken) { - checkProposedApiEnabled(extension, 'languageModels'); - token ??= CancellationToken.None; - options ??= {}; - return extHostLanguageModels.sendChatRequest(extension, languageModel, messages, options, token); - }, - computeTokenLength(languageModel: string, text: string | vscode.LanguageModelChatMessage, token?: vscode.CancellationToken) { - checkProposedApiEnabled(extension, 'languageModels'); - token ??= CancellationToken.None; - return extHostLanguageModels.computeTokenLength(languageModel, text, token); - }, - getLanguageModelInformation(languageModel: string) { - checkProposedApiEnabled(extension, 'languageModels'); - return extHostLanguageModels.getLanguageModelInfo(languageModel); - }, // --- embeddings get embeddingModels() { checkProposedApiEnabled(extension, 'embeddings'); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index a2c75722122..093c606d6f3 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1201,6 +1201,8 @@ export interface MainThreadLanguageModelsShape extends IDisposable { $unregisterProvider(handle: number): void; $handleProgressChunk(requestId: number, chunk: IChatResponseFragment): Promise; + $selectChatModels(selector: Partial): Promise; + $prepareChatAccess(extension: ExtensionIdentifier, providerId: string, justification?: string): Promise; $fetchResponse(extension: ExtensionIdentifier, provider: string, requestId: number, messages: IChatMessage[], options: {}, token: CancellationToken): Promise; @@ -1209,7 +1211,7 @@ export interface MainThreadLanguageModelsShape extends IDisposable { } export interface ExtHostLanguageModelsShape { - $updateLanguageModels(data: { added?: ILanguageModelChatMetadata[]; removed?: string[] }): void; + $acceptChatModelMetadata(data: { added?: ILanguageModelChatMetadata[]; removed?: string[] }): void; $updateModelAccesslist(data: { from: ExtensionIdentifier; to: ExtensionIdentifier; enabled: boolean }[]): void; $provideLanguageModelResponse(handle: number, requestId: number, from: ExtensionIdentifier, messages: IChatMessage[], options: { [name: string]: any }, token: CancellationToken): Promise; $handleResponseFragment(requestId: number, chunk: IChatResponseFragment): Promise; diff --git a/src/vs/workbench/api/common/extHostLanguageModels.ts b/src/vs/workbench/api/common/extHostLanguageModels.ts index eb3cd6373fb..f5a77e381b0 100644 --- a/src/vs/workbench/api/common/extHostLanguageModels.ts +++ b/src/vs/workbench/api/common/extHostLanguageModels.ts @@ -3,26 +3,26 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { AsyncIterableSource, Barrier } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { CancellationError } from 'vs/base/common/errors'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Iterable } from 'vs/base/common/iterator'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { localize } from 'vs/nls'; +import { ExtensionIdentifier, ExtensionIdentifierMap, ExtensionIdentifierSet, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ILogService } from 'vs/platform/log/common/log'; +import { Progress } from 'vs/platform/progress/common/progress'; import { ExtHostLanguageModelsShape, MainContext, MainThreadLanguageModelsShape } from 'vs/workbench/api/common/extHost.protocol'; +import { IExtHostAuthentication } from 'vs/workbench/api/common/extHostAuthentication'; +import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; -import type * as vscode from 'vscode'; -import { Progress } from 'vs/platform/progress/common/progress'; import { IChatMessage, IChatResponseFragment, ILanguageModelChatMetadata } from 'vs/workbench/contrib/chat/common/languageModels'; -import { ExtensionIdentifier, ExtensionIdentifierMap, ExtensionIdentifierSet, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { AsyncIterableSource, Barrier } from 'vs/base/common/async'; -import { Emitter, Event } from 'vs/base/common/event'; -import { localize } from 'vs/nls'; import { INTERNAL_AUTH_PROVIDER_PREFIX } from 'vs/workbench/services/authentication/common/authentication'; -import { CancellationError } from 'vs/base/common/errors'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; -import { IExtHostAuthentication } from 'vs/workbench/api/common/extHostAuthentication'; -import { ILogService } from 'vs/platform/log/common/log'; -import { Iterable } from 'vs/base/common/iterator'; import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; +import type * as vscode from 'vscode'; export interface IExtHostLanguageModels extends ExtHostLanguageModels { } @@ -110,7 +110,6 @@ class LanguageModelResponse { stream.resolve(); } } - } export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { @@ -121,11 +120,11 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { private readonly _proxy: MainThreadLanguageModelsShape; private readonly _onDidChangeModelAccess = new Emitter<{ from: ExtensionIdentifier; to: ExtensionIdentifier }>(); - private readonly _onDidChangeProviders = new Emitter(); + private readonly _onDidChangeProviders = new Emitter(); readonly onDidChangeProviders = this._onDidChangeProviders.event; private readonly _languageModels = new Map(); - private readonly _allLanguageModelData = new Map(); // these are ALL models, not just the one in this EH + private readonly _allLanguageModelData = new Map }>(); // these are ALL models, not just the one in this EH private readonly _modelAccessList = new ExtensionIdentifierMap(); private readonly _pendingRequest = new Map(); @@ -156,7 +155,9 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { this._proxy.$registerLanguageModelProvider(handle, identifier, { extension: extension.identifier, identifier: identifier, + vendor: metadata.vendor ?? ExtensionIdentifier.toKey(extension.identifier), name: metadata.name ?? '', + family: metadata.family ?? '', version: metadata.version, tokens: metadata.tokens, auth @@ -209,12 +210,12 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { //#region --- making request - $updateLanguageModels(data: { added?: ILanguageModelChatMetadata[] | undefined; removed?: string[] | undefined }): void { + $acceptChatModelMetadata(data: { added?: ILanguageModelChatMetadata[] | undefined; removed?: string[] | undefined }): void { const added: string[] = []; const removed: string[] = []; if (data.added) { for (const metadata of data.added) { - this._allLanguageModelData.set(metadata.identifier, metadata); + this._allLanguageModelData.set(metadata.identifier, { metadata, apiObjects: new ExtensionIdentifierMap() }); added.push(metadata.identifier); } } @@ -234,39 +235,65 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { } } - this._onDidChangeProviders.fire(Object.freeze({ - added: Object.freeze(added), - removed: Object.freeze(removed) - })); + this._onDidChangeProviders.fire(undefined); // TODO@jrieken@TylerLeonhardt - this is a temporary hack to populate the auth providers data.added?.forEach(this._fakeAuthPopulate, this); } - getLanguageModelIds(): string[] { - return Array.from(this._allLanguageModelData.keys()); - } + async selectLanguageModels(extension: IExtensionDescription, selector: vscode.LanguageModelChatSelector) { - $updateModelAccesslist(data: { from: ExtensionIdentifier; to: ExtensionIdentifier; enabled: boolean }[]): void { - const updated = new Array<{ from: ExtensionIdentifier; to: ExtensionIdentifier }>(); - for (const { from, to, enabled } of data) { - const set = this._modelAccessList.get(from) ?? new ExtensionIdentifierSet(); - const oldValue = set.has(to); - if (oldValue !== enabled) { - if (enabled) { - set.add(to); - } else { - set.delete(to); - } - this._modelAccessList.set(from, set); - const newItem = { from, to }; - updated.push(newItem); - this._onDidChangeModelAccess.fire(newItem); + // this triggers extension activation + const models = await this._proxy.$selectChatModels(selector); + + const result: vscode.LanguageModelChat[] = []; + const that = this; + for (const identifier of models) { + const data = this._allLanguageModelData.get(identifier); + if (!data) { + // model gone? is this an error on us? + continue; } + + let apiObject = data.apiObjects.get(extension.identifier); + + if (!apiObject) { + apiObject = { + id: identifier, + vendor: data.metadata.vendor, + family: data.metadata.family, + version: data.metadata.version, + name: data.metadata.name, + contextSize: data.metadata.tokens, + computeTokenLength(text, token) { + if (!that._allLanguageModelData.has(identifier)) { + throw extHostTypes.LanguageModelError.NotFound(identifier); + } + return that._computeTokenLength(identifier, text, token ?? CancellationToken.None); + }, + sendRequest(messages, options, token) { + if (!that._allLanguageModelData.has(identifier)) { + throw extHostTypes.LanguageModelError.NotFound(identifier); + } + return that._sendChatRequest(extension, identifier, messages, options ?? {}, token ?? CancellationToken.None); + } + }; + + Object.freeze(apiObject); + data.apiObjects.set(extension.identifier, apiObject); + } + + result.push(apiObject); } + + if (result.length === 0) { + return undefined; + } + + return result; } - async sendChatRequest(extension: IExtensionDescription, languageModelId: string, messages: (vscode.LanguageModelChatMessage | vscode.LanguageModelChatMessage2)[], options: vscode.LanguageModelChatRequestOptions, token: CancellationToken) { + private async _sendChatRequest(extension: IExtensionDescription, languageModelId: string, messages: vscode.LanguageModelChatMessage[], options: vscode.LanguageModelChatRequestOptions, token: CancellationToken) { const internalMessages: IChatMessage[] = this._convertMessages(extension, messages); @@ -329,17 +356,10 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { private _convertMessages(extension: IExtensionDescription, messages: vscode.LanguageModelChatMessage[]) { const internalMessages: IChatMessage[] = []; for (const message of messages) { - if (message instanceof extHostTypes.LanguageModelChatMessage) { - if (message.role as number === extHostTypes.LanguageModelChatMessageRole.System) { - checkProposedApiEnabled(extension, 'languageModelSystem'); - } - internalMessages.push(typeConvert.LanguageModelChatMessage.from(message)); - } else { - if (message instanceof extHostTypes.LanguageModelChatSystemMessage) { - checkProposedApiEnabled(extension, 'languageModelSystem'); - } - internalMessages.push(typeConvert.LanguageModelChatMessage.from(message)); + if (message.role as number === extHostTypes.LanguageModelChatMessageRole.System) { + checkProposedApiEnabled(extension, 'languageModelSystem'); } + internalMessages.push(typeConvert.LanguageModelChatMessage.from(message)); } return internalMessages; } @@ -399,7 +419,7 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { } } - async computeTokenLength(languageModelId: string, value: string | vscode.LanguageModelChatMessage, token: vscode.CancellationToken): Promise { + private async _computeTokenLength(languageModelId: string, value: string | vscode.LanguageModelChatMessage, token: vscode.CancellationToken): Promise { const data = this._allLanguageModelData.get(languageModelId); if (!data) { @@ -412,21 +432,26 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { return local.provider.provideTokenCount(value, token); } - return this._proxy.$countTokens(data.identifier, (typeof value === 'string' ? value : typeConvert.LanguageModelChatMessage.from(value)), token); + return this._proxy.$countTokens(data.metadata.identifier, (typeof value === 'string' ? value : typeConvert.LanguageModelChatMessage.from(value)), token); } - getLanguageModelInfo(languageModelId: string): vscode.LanguageModelInformation | undefined { - const data = this._allLanguageModelData.get(languageModelId); - if (!data) { - return undefined; + $updateModelAccesslist(data: { from: ExtensionIdentifier; to: ExtensionIdentifier; enabled: boolean }[]): void { + const updated = new Array<{ from: ExtensionIdentifier; to: ExtensionIdentifier }>(); + for (const { from, to, enabled } of data) { + const set = this._modelAccessList.get(from) ?? new ExtensionIdentifierSet(); + const oldValue = set.has(to); + if (oldValue !== enabled) { + if (enabled) { + set.add(to); + } else { + set.delete(to); + } + this._modelAccessList.set(from, set); + const newItem = { from, to }; + updated.push(newItem); + this._onDidChangeModelAccess.fire(newItem); + } } - - return Object.freeze({ - id: data.identifier, - name: data.name, - version: data.version, - contextLength: data.tokens, - }); } private readonly _languageAccessInformationExtensions = new Set>(); @@ -449,7 +474,7 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { if (!data) { return undefined; } - if (!that._isUsingAuth(from.identifier, data)) { + if (!that._isUsingAuth(from.identifier, data.metadata)) { return true; } @@ -457,7 +482,7 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { if (!list) { return undefined; } - return list.has(data.extension); + return list.has(data.metadata.extension); } }; } diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 4d7ec217589..48befe30b24 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -2242,7 +2242,7 @@ export namespace ChatFollowup { export namespace LanguageModelChatMessage { - export function to(message: chatProvider.IChatMessage): vscode.LanguageModelChatMessage2 { + export function to(message: chatProvider.IChatMessage): vscode.LanguageModelChatMessage { switch (message.role) { case chatProvider.ChatMessageRole.System: return new types.LanguageModelChatMessage(types.LanguageModelChatMessageRole.System, message.content); case chatProvider.ChatMessageRole.User: return new types.LanguageModelChatMessage(types.LanguageModelChatMessageRole.User, message.content); @@ -2250,7 +2250,7 @@ export namespace LanguageModelChatMessage { } } - export function from(message: vscode.LanguageModelChatMessage2): chatProvider.IChatMessage { + export function from(message: vscode.LanguageModelChatMessage): chatProvider.IChatMessage { switch (message.role as types.LanguageModelChatMessageRole) { case types.LanguageModelChatMessageRole.System: return { role: chatProvider.ChatMessageRole.System, content: message.content }; case types.LanguageModelChatMessageRole.User: return { role: chatProvider.ChatMessageRole.User, content: message.content }; diff --git a/src/vs/workbench/contrib/chat/common/languageModels.ts b/src/vs/workbench/contrib/chat/common/languageModels.ts index a538c2aa93a..240aa33e3ba 100644 --- a/src/vs/workbench/contrib/chat/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/common/languageModels.ts @@ -6,9 +6,11 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { isEmptyObject } from 'vs/base/common/types'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IProgress } from 'vs/platform/progress/common/progress'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; export const enum ChatMessageRole { System, @@ -28,9 +30,11 @@ export interface IChatResponseFragment { export interface ILanguageModelChatMetadata { readonly extension: ExtensionIdentifier; - readonly identifier: string; readonly name: string; + readonly identifier: string; + readonly vendor: string; readonly version: string; + readonly family: string; readonly tokens: number; readonly auth?: { @@ -57,6 +61,8 @@ export interface ILanguageModelsService { lookupLanguageModel(identifier: string): ILanguageModelChatMetadata | undefined; + selectLanguageModels(selector: Partial): Promise; + registerLanguageModelChat(identifier: string, provider: ILanguageModelChat): IDisposable; makeLanguageModelChatRequest(identifier: string, from: ExtensionIdentifier, messages: IChatMessage[], options: { [name: string]: any }, progress: IProgress, token: CancellationToken): Promise; @@ -72,6 +78,10 @@ export class LanguageModelsService implements ILanguageModelsService { private readonly _onDidChangeProviders = new Emitter<{ added?: ILanguageModelChatMetadata[]; removed?: string[] }>(); readonly onDidChangeLanguageModels: Event<{ added?: ILanguageModelChatMetadata[]; removed?: string[] }> = this._onDidChangeProviders.event; + constructor( + @IExtensionService private readonly _extensionService: IExtensionService, + ) { } + dispose() { this._onDidChangeProviders.dispose(); this._providers.clear(); @@ -85,6 +95,29 @@ export class LanguageModelsService implements ILanguageModelsService { return this._providers.get(identifier)?.metadata; } + async selectLanguageModels(selector: Partial): Promise { + await this._extensionService.activateByEvent(`onLanguageModelChat:${selector.vendor ?? '*'}}`); + + const result: string[] = []; + + for (const model of this._providers.values()) { + if (selector.vendor !== undefined && model.metadata.vendor === selector.vendor + || selector.family !== undefined && model.metadata.family === selector.family + || selector.version !== undefined && model.metadata.version === selector.version + || selector.identifier !== undefined && model.metadata.identifier === selector.identifier + ) { + // true selection + result.push(model.metadata.identifier); + + } else if (!selector || isEmptyObject(selector)) { + // no selection + result.push(model.metadata.identifier); + } + } + + return result; + } + registerLanguageModelChat(identifier: string, provider: ILanguageModelChat): IDisposable { if (this._providers.has(identifier)) { throw new Error(`Chat response provider with identifier ${identifier} is already registered.`); diff --git a/src/vscode-dts/vscode.proposed.chatProvider.d.ts b/src/vscode-dts/vscode.proposed.chatProvider.d.ts index 812f4a001a7..19f3d40d056 100644 --- a/src/vscode-dts/vscode.proposed.chatProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatProvider.d.ts @@ -19,20 +19,30 @@ declare module 'vscode' { onDidReceiveLanguageModelResponse2?: Event<{ readonly extensionId: string; readonly participant?: string; readonly tokenCount?: number }>; - provideLanguageModelResponse(messages: LanguageModelChatMessage2[], options: { [name: string]: any }, extensionId: string, progress: Progress, token: CancellationToken): Thenable; + provideLanguageModelResponse(messages: LanguageModelChatMessage[], options: { [name: string]: any }, extensionId: string, progress: Progress, token: CancellationToken): Thenable; provideTokenCount(text: string | LanguageModelChatMessage, token: CancellationToken): Thenable; } export interface ChatResponseProviderMetadata { - /** - * The name of the model that is used for this chat access. It is expected that the model name can - * be used to lookup properties like token limits and ChatML support - */ - // TODO@API rename to model - name: string; - version: string; + readonly vendor: string; + + /** + * Human-readable name of the language model. + */ + readonly name: string; + /** + * Opaque family-name of the language model. Values might be `gpt-3.5-turbo`, `gpt4`, `phi2`, or `llama` + * but they are defined by extensions contributing languages and subject to change. + */ + readonly family: string; + + /** + * Opaque version string of the model. This is defined by the extension contributing the language model + * and subject to change while the identifier is stable. + */ + readonly version: string; tokens: number; diff --git a/src/vscode-dts/vscode.proposed.languageModels.d.ts b/src/vscode-dts/vscode.proposed.languageModels.d.ts index 675e46967b1..81d08344692 100644 --- a/src/vscode-dts/vscode.proposed.languageModels.d.ts +++ b/src/vscode-dts/vscode.proposed.languageModels.d.ts @@ -66,12 +66,6 @@ declare module 'vscode' { Assistant = 2 } - /** - * @deprecated - */ - // TODO@API remove - export type LanguageModelChatMessage2 = LanguageModelChatMessage; - /** * Represents a message in a chat. Can assume different roles, like user or assistant. */ @@ -101,179 +95,89 @@ declare module 'vscode' { constructor(role: LanguageModelChatMessageRole, content: string, name?: string); } - /** - * Represents information about a registered language model. - */ - export interface LanguageModelInformation { + // --------------------------- + // Language Model Object (V2) + // (+) can pick by id or family + // (++) makes it harder to hardcode an identifier of a model in source code + + + // TODO@API name LanguageModelChatEndpoint + export interface LanguageModelChat { /** - * The identifier of the language model. + * Opaque identifier of the language model. */ readonly id: string; /** - * The human-readable name of the language model. + * A well-know identifier of the vendor of the language model, a sample is `copilot`, but + * values are defined by extensions contributing chat model and need to be looked up with them. + */ + readonly vendor: string; + /** + * Human-readable name of the language model. */ readonly name: string; - + /** + * Opaque family-name of the language model. Values might be `gpt-3.5-turbo`, `gpt4`, `phi2`, or `llama` + * but they are defined by extensions contributing languages and subject to change. + */ + readonly family: string; /** * Opaque version string of the model. This is defined by the extension contributing the language model * and subject to change while the identifier is stable. */ readonly version: string; - /** - * Opaque family-name of the language model. Values might be `gpt-3.5-turbe`, `gpt4`, `phi2`, or `llama` - * but they are defined by extensions contributing languages and subject to change. - */ - family: string; - - /** - * The number of available tokens that can be used when sending requests - * to the language model. - * - * _Note_ that input- and output-tokens count towards this limit. - * - * @see {@link lm.sendChatRequest} - */ - // TODO@API CAPI only defines prompt_token_count which IMO is just input-tokens - readonly contextLength: number; - } - - // --------------------------- - // Language Model Object (V1) - - export interface LanguageModelInformation2 { - /** - * Human-readable name of the language model. - */ - name: string; - /** - * Opaque family-name of the language model. Values might be `gpt-3.5-turbe`, `gpt4`, `phi2`, or `llama` - * but they are defined by extensions contributing languages and subject to change. - */ - family: string; - /** - * Opaque version string of the model. This is defined by the extension contributing the language model - * and subject to change while the identifier is stable. - */ - version: string; - } - - export interface LanguageModel { - - // TODO@API no id-property needed - readonly id: string; - - readonly info: LanguageModelInformation2; - - sendChatRequest(messages: LanguageModelChatMessage[], options?: LanguageModelChatRequestOptions, token?: CancellationToken): Thenable; - - // maybe optional - computeTokenLength(text: string | LanguageModelChatMessage, token?: CancellationToken): Thenable; - } - - export namespace lm { - - // TODO@API cannot enforce unique language model families, e.g how do we tell `openai.gpt4` apart from `copilot.gpt4` - export function getLanguageModel(family: string): LanguageModel[] | undefined; - - export const languageModels2: LanguageModel[]; - } - // --------------------------- - - - // --------------------------- - // Language Model Object (V2) - // (+) can pick by id or family - // (++) makes it harder to hardcode an identifier of a model in source code - - export interface LanguageModelInformation2 { - /** - * Opaque identifier of the language model. - */ - readonly id: string; - /** - * Human-readable name of the language model. - */ - name: string; - /** - * Opaque family-name of the language model. Values might be `gpt-3.5-turbo`, `gpt4`, `phi2`, or `llama` - * but they are defined by extensions contributing languages and subject to change. - */ - family: string; - /** - * Opaque version string of the model. This is defined by the extension contributing the language model - * and subject to change while the identifier is stable. - */ - version: string; - } - - export interface LanguageModel3 { - /** - * Opaque identifier of the language model. - */ - readonly id: string; - vendor?: string; - /** - * Human-readable name of the language model. - */ - name: string; - /** - * Opaque family-name of the language model. Values might be `gpt-3.5-turbo`, `gpt4`, `phi2`, or `llama` - * but they are defined by extensions contributing languages and subject to change. - */ - family: string; - /** - * Opaque version string of the model. This is defined by the extension contributing the language model - * and subject to change while the identifier is stable. - */ - version: string; - - // TODO@API // max_prompt_tokens vs output_tokens vs context_size - contextSize: number; + readonly contextSize: number; - sendChatRequest2(messages: LanguageModelChatMessage[], options?: LanguageModelChatRequestOptions, token?: CancellationToken): Thenable; + /** + * Make a chat request using a language model. + * + * - *Note 1:* language model use may be subject to access restrictions and user consent. Calling this function + * for the first time (for a extension) will show a consent dialog to the user and because of that this function + * must _only be called in response to a user action!_ Extension can use {@link LanguageModelAccessInformation.canSendRequest} + * to check if they have the necessary permissions to make a request. + * + * - *Note 2:* language models are contributed by other extensions and as they evolve and change, + * the set of available language models may change over time. Therefore it is strongly recommend to check + * {@link languageModels} for available values and handle missing language models gracefully. + * + * This function will return a rejected promise if making a request to the language model is not + * possible. Reasons for this can be: + * + * - user consent not given, see {@link LanguageModelError.NoPermissions `NoPermissions`} + * - model does not exist anymore, see {@link LanguageModelError.NotFound `NotFound`} + * - quota limits exceeded, see {@link LanguageModelError.Blocked `Blocked`} + * - other issues in which case extension must check {@link LanguageModelError.cause `LanguageModelError.cause`} + * + * @param messages An array of message instances. + * @param options Options that control the request. + * @param token A cancellation token which controls the request. See {@link CancellationTokenSource} for how to create one. + * @returns A thenable that resolves to a {@link LanguageModelChatResponse}. The promise will reject when the request couldn't be made. + */ + sendRequest(messages: LanguageModelChatMessage[], options?: LanguageModelChatRequestOptions, token?: CancellationToken): Thenable; + /** + * Uses the model specific tokenzier and computes the length in tokens of a given message. + * + * @param text A string or a message instance. + * @param token Optional cancellation token. + * @returns A thenable that resolves to the length of the message in tokens. + */ + // TODO@API `undefined` when the language model does not support computing token length + // ollama has nothing + // anthropic suggests to count after the fact https://github.com/anthropics/anthropic-tokenizer-typescript?tab=readme-ov-file#anthropic-typescript-tokenizer computeTokenLength(text: string | LanguageModelChatMessage, token?: CancellationToken): Thenable; } - export namespace lm { - // export const languageModels3: readonly LanguageModelInformation2[]; - - // export const onDidChangeLanguageModels3: Event<{ readonly added: readonly LanguageModelInformation2[]; readonly removed: readonly LanguageModelInformation2[] }>; - - // export const onDidChangeLanguageModels3: Event; - - // (++) lazy activation - // (++) give specific LM to some extension - // // variant A - // export function fetchLanguageModel(selector: { id?: string; family?: string; version?: string }): Thenable; - - // // variant B - export function fetchLanguageModel(selector: { vendor: string; family?: string; version?: string; id?: string }): Thenable; - - // export function sendChatRequest2(languageModel: LanguageModelInformation2, messages: LanguageModelChatMessage[], options?: LanguageModelChatRequestOptions, token?: CancellationToken): Thenable; - - // export function computeTokenLength(languageModel: LanguageModelInformation2, text: string | LanguageModelChatMessage, token?: CancellationToken): Thenable; - } - // --------------------------- - - /** - * An event describing the change in the set of available language models. - */ - // TODO@API use LanguageModelInformation instead of string? - export interface LanguageModelChangeEvent { - /** - * Added language models. - */ - readonly added: readonly string[]; - /** - * Removed language models. - */ - readonly removed: readonly string[]; + export interface LanguageModelChatSelector { + vendor?: string; // TODO@API make required? + family?: string; + version?: string; + id?: string; } /** @@ -337,65 +241,57 @@ declare module 'vscode' { export namespace lm { /** - * The identifiers of all language models that are currently available. + * An event that is fired when the set of available chat models changes. */ - export const languageModels: readonly string[]; + export const onDidChangeChatModels: Event; - /** - * An event that is fired when the set of available language models changes. - */ - export const onDidChangeLanguageModels: Event; + // // variant B + // (++) lazy activation + // (++) give specific LM to some extension + export function selectLanguageModels(selector: LanguageModelChatSelector): Thenable; - /** - * Retrieve information about a language model. - * - * @param languageModel A language model identifier. - * @returns A {@link LanguageModelInformation} instance or `undefined` if the language model does not exist. - */ - export function getLanguageModelInformation(languageModel: string): LanguageModelInformation | undefined; + // /** + // * Make a chat request using a language model. + // * + // * - *Note 1:* language model use may be subject to access restrictions and user consent. Calling this function + // * for the first time (for a extension) will show a consent dialog to the user and because of that this function + // * must _only be called in response to a user action!_ Extension can use {@link LanguageModelAccessInformation.canSendRequest} + // * to check if they have the necessary permissions to make a request. + // * + // * - *Note 2:* language models are contributed by other extensions and as they evolve and change, + // * the set of available language models may change over time. Therefore it is strongly recommend to check + // * {@link languageModels} for available values and handle missing language models gracefully. + // * + // * This function will return a rejected promise if making a request to the language model is not + // * possible. Reasons for this can be: + // * + // * - user consent not given, see {@link LanguageModelError.NoPermissions `NoPermissions`} + // * - model does not exist, see {@link LanguageModelError.NotFound `NotFound`} + // * - quota limits exceeded, see {@link LanguageModelError.Blocked `Blocked`} + // * - other issues in which case extension must check {@link LanguageModelError.cause `LanguageModelError.cause`} + // * + // * @param languageModel A language model identifier. + // * @param messages An array of message instances. + // * @param options Options that control the request. + // * @param token A cancellation token which controls the request. See {@link CancellationTokenSource} for how to create one. + // * @returns A thenable that resolves to a {@link LanguageModelChatResponse}. The promise will reject when the request couldn't be made. + // */ + // export function sendChatRequest(languageModel: string, messages: LanguageModelChatMessage[], options?: LanguageModelChatRequestOptions, token?: CancellationToken): Thenable; - /** - * Make a chat request using a language model. - * - * - *Note 1:* language model use may be subject to access restrictions and user consent. Calling this function - * for the first time (for a extension) will show a consent dialog to the user and because of that this function - * must _only be called in response to a user action!_ Extension can use {@link LanguageModelAccessInformation.canSendRequest} - * to check if they have the necessary permissions to make a request. - * - * - *Note 2:* language models are contributed by other extensions and as they evolve and change, - * the set of available language models may change over time. Therefore it is strongly recommend to check - * {@link languageModels} for available values and handle missing language models gracefully. - * - * This function will return a rejected promise if making a request to the language model is not - * possible. Reasons for this can be: - * - * - user consent not given, see {@link LanguageModelError.NoPermissions `NoPermissions`} - * - model does not exist, see {@link LanguageModelError.NotFound `NotFound`} - * - quota limits exceeded, see {@link LanguageModelError.Blocked `Blocked`} - * - other issues in which case extension must check {@link LanguageModelError.cause `LanguageModelError.cause`} - * - * @param languageModel A language model identifier. - * @param messages An array of message instances. - * @param options Options that control the request. - * @param token A cancellation token which controls the request. See {@link CancellationTokenSource} for how to create one. - * @returns A thenable that resolves to a {@link LanguageModelChatResponse}. The promise will reject when the request couldn't be made. - */ - export function sendChatRequest(languageModel: string, messages: LanguageModelChatMessage[], options?: LanguageModelChatRequestOptions, token?: CancellationToken): Thenable; - - /** - * Uses the language model specific tokenzier and computes the length in token of a given message. - * - * *Note* that this function will throw when the language model does not exist. - * - * @param languageModel A language model identifier. - * @param text A string or a message instance. - * @param token Optional cancellation token. - * @returns A thenable that resolves to the length of the message in tokens. - */ - // TODO@API `undefined` when the language model does not support computing token length - // ollama has nothing - // anthropic suggests to count after the fact https://github.com/anthropics/anthropic-tokenizer-typescript?tab=readme-ov-file#anthropic-typescript-tokenizer - export function computeTokenLength(languageModel: string, text: string | LanguageModelChatMessage, token?: CancellationToken): Thenable; + // /** + // * Uses the language model specific tokenzier and computes the length in token of a given message. + // * + // * *Note* that this function will throw when the language model does not exist. + // * + // * @param languageModel A language model identifier. + // * @param text A string or a message instance. + // * @param token Optional cancellation token. + // * @returns A thenable that resolves to the length of the message in tokens. + // */ + // // TODO@API `undefined` when the language model does not support computing token length + // // ollama has nothing + // // anthropic suggests to count after the fact https://github.com/anthropics/anthropic-tokenizer-typescript?tab=readme-ov-file#anthropic-typescript-tokenizer + // export function computeTokenLength(languageModel: string, text: string | LanguageModelChatMessage, token?: CancellationToken): Thenable; } /** @@ -417,6 +313,7 @@ declare module 'vscode' { * @return `true` if a request can be made, `false` if not, `undefined` if the language * model does not exist or consent hasn't been asked for. */ + // TODO@API applies to chat and embeddings models canSendRequest(languageModelId: string): boolean | undefined; } From 699092369961fa1783b0bd38963a494b6c86dc75 Mon Sep 17 00:00:00 2001 From: Johannes Date: Wed, 8 May 2024 12:04:21 +0200 Subject: [PATCH 042/357] spell-out selector type, allow to select/filter on specific extensions --- .../api/browser/mainThreadLanguageModels.ts | 4 +- .../workbench/api/common/extHost.api.impl.ts | 2 +- .../workbench/api/common/extHost.protocol.ts | 4 +- .../api/common/extHostLanguageModels.ts | 5 +- .../contrib/chat/common/languageModels.ts | 16 +++++- .../vscode.proposed.chatProvider.d.ts | 7 +-- .../vscode.proposed.languageModels.d.ts | 54 ++++--------------- 7 files changed, 35 insertions(+), 57 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadLanguageModels.ts b/src/vs/workbench/api/browser/mainThreadLanguageModels.ts index 5c1f405b019..964628c863a 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageModels.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageModels.ts @@ -13,7 +13,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IProgress, Progress } from 'vs/platform/progress/common/progress'; import { ExtHostLanguageModelsShape, ExtHostContext, MainContext, MainThreadLanguageModelsShape } from 'vs/workbench/api/common/extHost.protocol'; import { ILanguageModelStatsService } from 'vs/workbench/contrib/chat/common/languageModelStats'; -import { ILanguageModelChatMetadata, IChatResponseFragment, ILanguageModelsService, IChatMessage } from 'vs/workbench/contrib/chat/common/languageModels'; +import { ILanguageModelChatMetadata, IChatResponseFragment, ILanguageModelsService, IChatMessage, ILanguageModelChatSelector } from 'vs/workbench/contrib/chat/common/languageModels'; import { IAuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService'; import { AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationProviderCreateSessionOptions, IAuthenticationService, INTERNAL_AUTH_PROVIDER_PREFIX } from 'vs/workbench/services/authentication/common/authentication'; import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; @@ -78,7 +78,7 @@ export class MainThreadLanguageModels implements MainThreadLanguageModelsShape { this._providerRegistrations.deleteAndDispose(handle); } - $selectChatModels(selector: Partial): Promise { + $selectChatModels(selector: ILanguageModelChatSelector): Promise { return this._chatProviderService.selectLanguageModels(selector); } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 2fc249f0d2a..f5d4f04d280 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1428,7 +1428,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I // namespace: lm const lm: typeof vscode.lm = { - selectLanguageModels: (selector) => { + selectChatModels: (selector) => { checkProposedApiEnabled(extension, 'languageModels'); return extHostLanguageModels.selectLanguageModels(extension, selector); }, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 093c606d6f3..e4a7a398823 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -54,7 +54,7 @@ import { ChatAgentLocation, IChatAgentMetadata, IChatAgentRequest, IChatAgentRes import { IChatProgressResponseContent } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChatFollowup, IChatProgress, IChatResponseErrorDetails, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatRequestVariableValue, IChatVariableData, IChatVariableResolverProgress } from 'vs/workbench/contrib/chat/common/chatVariables'; -import { IChatMessage, IChatResponseFragment, ILanguageModelChatMetadata } from 'vs/workbench/contrib/chat/common/languageModels'; +import { IChatMessage, IChatResponseFragment, ILanguageModelChatMetadata, ILanguageModelChatSelector } from 'vs/workbench/contrib/chat/common/languageModels'; import { DebugConfigurationProviderTriggerKind, IAdapterDescriptor, IConfig, IDebugSessionReplMode, IDebugVisualization, IDebugVisualizationContext, IDebugVisualizationTreeItem, MainThreadDebugVisualization } from 'vs/workbench/contrib/debug/common/debug'; import * as notebookCommon from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CellExecutionUpdateType } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; @@ -1201,7 +1201,7 @@ export interface MainThreadLanguageModelsShape extends IDisposable { $unregisterProvider(handle: number): void; $handleProgressChunk(requestId: number, chunk: IChatResponseFragment): Promise; - $selectChatModels(selector: Partial): Promise; + $selectChatModels(selector: ILanguageModelChatSelector): Promise; $prepareChatAccess(extension: ExtensionIdentifier, providerId: string, justification?: string): Promise; $fetchResponse(extension: ExtensionIdentifier, provider: string, requestId: number, messages: IChatMessage[], options: {}, token: CancellationToken): Promise; diff --git a/src/vs/workbench/api/common/extHostLanguageModels.ts b/src/vs/workbench/api/common/extHostLanguageModels.ts index f5a77e381b0..d19e5ee4d99 100644 --- a/src/vs/workbench/api/common/extHostLanguageModels.ts +++ b/src/vs/workbench/api/common/extHostLanguageModels.ts @@ -160,7 +160,8 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { family: metadata.family ?? '', version: metadata.version, tokens: metadata.tokens, - auth + auth, + targetExtensions: metadata.extensions }); const responseReceivedListener = provider.onDidReceiveLanguageModelResponse2?.(({ extensionId, participant, tokenCount }) => { @@ -244,7 +245,7 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { async selectLanguageModels(extension: IExtensionDescription, selector: vscode.LanguageModelChatSelector) { // this triggers extension activation - const models = await this._proxy.$selectChatModels(selector); + const models = await this._proxy.$selectChatModels({ ...selector, extension: extension.identifier }); const result: vscode.LanguageModelChat[] = []; const that = this; diff --git a/src/vs/workbench/contrib/chat/common/languageModels.ts b/src/vs/workbench/contrib/chat/common/languageModels.ts index 240aa33e3ba..f6ba87f122b 100644 --- a/src/vs/workbench/contrib/chat/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/common/languageModels.ts @@ -36,6 +36,7 @@ export interface ILanguageModelChatMetadata { readonly version: string; readonly family: string; readonly tokens: number; + readonly targetExtensions?: string[]; readonly auth?: { readonly providerLabel: string; @@ -49,6 +50,16 @@ export interface ILanguageModelChat { provideTokenCount(message: string | IChatMessage, token: CancellationToken): Promise; } +export interface ILanguageModelChatSelector { + readonly name?: string; + readonly identifier?: string; + readonly vendor?: string; + readonly version?: string; + readonly family?: string; + readonly tokens?: number; + readonly extension?: ExtensionIdentifier; +} + export const ILanguageModelsService = createDecorator('ILanguageModelsService'); export interface ILanguageModelsService { @@ -61,7 +72,7 @@ export interface ILanguageModelsService { lookupLanguageModel(identifier: string): ILanguageModelChatMetadata | undefined; - selectLanguageModels(selector: Partial): Promise; + selectLanguageModels(selector: ILanguageModelChatSelector): Promise; registerLanguageModelChat(identifier: string, provider: ILanguageModelChat): IDisposable; @@ -95,7 +106,7 @@ export class LanguageModelsService implements ILanguageModelsService { return this._providers.get(identifier)?.metadata; } - async selectLanguageModels(selector: Partial): Promise { + async selectLanguageModels(selector: ILanguageModelChatSelector): Promise { await this._extensionService.activateByEvent(`onLanguageModelChat:${selector.vendor ?? '*'}}`); const result: string[] = []; @@ -105,6 +116,7 @@ export class LanguageModelsService implements ILanguageModelsService { || selector.family !== undefined && model.metadata.family === selector.family || selector.version !== undefined && model.metadata.version === selector.version || selector.identifier !== undefined && model.metadata.identifier === selector.identifier + || selector.extension !== undefined && model.metadata.targetExtensions?.some(candidate => ExtensionIdentifier.equals(candidate, selector.extension)) ) { // true selection result.push(model.metadata.identifier); diff --git a/src/vscode-dts/vscode.proposed.chatProvider.d.ts b/src/vscode-dts/vscode.proposed.chatProvider.d.ts index 19f3d40d056..55f0a2a383b 100644 --- a/src/vscode-dts/vscode.proposed.chatProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatProvider.d.ts @@ -52,11 +52,12 @@ declare module 'vscode' { * Additionally, the extension can provide a label that will be shown in the UI. */ auth?: true | { label: string }; - - // MAGIC - extension?: string; } + export interface ChatResponseProviderMetadata { + // limit this provider to some extensions + extensions: string[]; + } export namespace chat { diff --git a/src/vscode-dts/vscode.proposed.languageModels.d.ts b/src/vscode-dts/vscode.proposed.languageModels.d.ts index 81d08344692..df47fd0cec7 100644 --- a/src/vscode-dts/vscode.proposed.languageModels.d.ts +++ b/src/vscode-dts/vscode.proposed.languageModels.d.ts @@ -178,6 +178,7 @@ declare module 'vscode' { family?: string; version?: string; id?: string; + // TODO@API tokens? min/max etc } /** @@ -245,53 +246,16 @@ declare module 'vscode' { */ export const onDidChangeChatModels: Event; - // // variant B + /** + * Select chat models by a {@link LanguageModelChatSelector selector}. This can yield in multiple or no chat models + * and extension must handle these cases, esp when no chat model exists. + * + * @param selector A chat model selector. + * @returns An array of chat models or `undefined` when no chat model was selected. + */ // (++) lazy activation // (++) give specific LM to some extension - export function selectLanguageModels(selector: LanguageModelChatSelector): Thenable; - - // /** - // * Make a chat request using a language model. - // * - // * - *Note 1:* language model use may be subject to access restrictions and user consent. Calling this function - // * for the first time (for a extension) will show a consent dialog to the user and because of that this function - // * must _only be called in response to a user action!_ Extension can use {@link LanguageModelAccessInformation.canSendRequest} - // * to check if they have the necessary permissions to make a request. - // * - // * - *Note 2:* language models are contributed by other extensions and as they evolve and change, - // * the set of available language models may change over time. Therefore it is strongly recommend to check - // * {@link languageModels} for available values and handle missing language models gracefully. - // * - // * This function will return a rejected promise if making a request to the language model is not - // * possible. Reasons for this can be: - // * - // * - user consent not given, see {@link LanguageModelError.NoPermissions `NoPermissions`} - // * - model does not exist, see {@link LanguageModelError.NotFound `NotFound`} - // * - quota limits exceeded, see {@link LanguageModelError.Blocked `Blocked`} - // * - other issues in which case extension must check {@link LanguageModelError.cause `LanguageModelError.cause`} - // * - // * @param languageModel A language model identifier. - // * @param messages An array of message instances. - // * @param options Options that control the request. - // * @param token A cancellation token which controls the request. See {@link CancellationTokenSource} for how to create one. - // * @returns A thenable that resolves to a {@link LanguageModelChatResponse}. The promise will reject when the request couldn't be made. - // */ - // export function sendChatRequest(languageModel: string, messages: LanguageModelChatMessage[], options?: LanguageModelChatRequestOptions, token?: CancellationToken): Thenable; - - // /** - // * Uses the language model specific tokenzier and computes the length in token of a given message. - // * - // * *Note* that this function will throw when the language model does not exist. - // * - // * @param languageModel A language model identifier. - // * @param text A string or a message instance. - // * @param token Optional cancellation token. - // * @returns A thenable that resolves to the length of the message in tokens. - // */ - // // TODO@API `undefined` when the language model does not support computing token length - // // ollama has nothing - // // anthropic suggests to count after the fact https://github.com/anthropics/anthropic-tokenizer-typescript?tab=readme-ov-file#anthropic-typescript-tokenizer - // export function computeTokenLength(languageModel: string, text: string | LanguageModelChatMessage, token?: CancellationToken): Thenable; + export function selectChatModels(selector: LanguageModelChatSelector): Thenable; } /** From 1a8d6b95a04595ae06685363653ed0ed438184b8 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 8 May 2024 12:14:56 +0200 Subject: [PATCH 043/357] update distro (#212254) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e9f2ac5579c..a421fc39606 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.90.0", - "distro": "600b5db066f5f4807121e5a3da0fa4bb7ed0eb1f", + "distro": "75eed367612c65f6edd69c54373da84495e0d0b8", "author": { "name": "Microsoft Corporation" }, From 4baa94788e572aadc2f3598c4f16972f0f3475df Mon Sep 17 00:00:00 2001 From: Johannes Date: Wed, 8 May 2024 15:33:33 +0200 Subject: [PATCH 044/357] register vendors statically and active extensions on them --- .../contrib/chat/common/languageModels.ts | 111 +++++++++++++++++- .../vscode.proposed.languageModels.d.ts | 22 ++-- 2 files changed, 114 insertions(+), 19 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/languageModels.ts b/src/vs/workbench/contrib/chat/common/languageModels.ts index f6ba87f122b..b5b90dcf1c5 100644 --- a/src/vs/workbench/contrib/chat/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/common/languageModels.ts @@ -5,12 +5,16 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; +import { Iterable } from 'vs/base/common/iterator'; +import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { isEmptyObject } from 'vs/base/common/types'; +import { isFalsyOrWhitespace } from 'vs/base/common/strings'; +import { localize } from 'vs/nls'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IProgress } from 'vs/platform/progress/common/progress'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IExtensionService, isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; +import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; export const enum ChatMessageRole { System, @@ -81,17 +85,93 @@ export interface ILanguageModelsService { computeTokenLength(identifier: string, message: string | IChatMessage, token: CancellationToken): Promise; } +const languageModelType: IJSONSchema = { + type: 'object', + properties: { + vendor: { + type: 'string', + description: localize('vscode.extension.contributes.languageModels.vendor', "A globally unique vendor of language models.") + } + } +}; + +interface IUserFriendlyLanguageModel { + vendor: string; +} + +export const languageModelExtensionPoint = ExtensionsRegistry.registerExtensionPoint({ + extensionPoint: 'languageModels', + jsonSchema: { + description: localize('vscode.extension.contributes.languageModels', "Contribute language models of a specific vendor."), + oneOf: [ + languageModelType, + { + type: 'array', + items: languageModelType + } + ] + }, + activationEventsGenerator: (contribs: IUserFriendlyLanguageModel[], result: { push(item: string): void }) => { + for (const contrib of contribs) { + result.push(`onLanguageModel:${contrib.vendor}`); + } + } +}); + export class LanguageModelsService implements ILanguageModelsService { + readonly _serviceBrand: undefined; - private readonly _providers: Map = new Map(); + private readonly _providers = new Map(); + private readonly _vendors = new Set(); private readonly _onDidChangeProviders = new Emitter<{ added?: ILanguageModelChatMetadata[]; removed?: string[] }>(); readonly onDidChangeLanguageModels: Event<{ added?: ILanguageModelChatMetadata[]; removed?: string[] }> = this._onDidChangeProviders.event; constructor( @IExtensionService private readonly _extensionService: IExtensionService, - ) { } + ) { + + languageModelExtensionPoint.setHandler((extensions) => { + + this._vendors.clear(); + + for (const extension of extensions) { + + if (!isProposedApiEnabled(extension.description, 'chatProvider')) { + extension.collector.error(localize('vscode.extension.contributes.languageModels.chatProviderRequired', "This contribution point requires the 'chatProvider' proposal.")); + continue; + } + + for (const item of Iterable.wrap(extension.value)) { + if (this._vendors.has(item.vendor)) { + extension.collector.error(localize('vscode.extension.contributes.languageModels.vendorAlreadyRegistered', "The vendor '{0}' is already registered and cannot be registered twice", item.vendor)); + continue; + } + if (isFalsyOrWhitespace(item.vendor)) { + extension.collector.error(localize('vscode.extension.contributes.languageModels.emptyVendor', "The vendor field cannot be empty.")); + continue; + } + if (item.vendor.trim() !== item.vendor) { + extension.collector.error(localize('vscode.extension.contributes.languageModels.whitespaceVendor', "The vendor field cannot start or end with whitespace.")); + continue; + } + this._vendors.add(item.vendor); + } + } + + const removed: string[] = []; + for (const [key, value] of this._providers) { + if (!this._vendors.has(value.metadata.vendor)) { + this._providers.delete(key); + removed.push(key); + } + } + if (removed.length > 0) { + this._onDidChangeProviders.fire({ removed }); + } + }); + } dispose() { this._onDidChangeProviders.dispose(); @@ -107,11 +187,20 @@ export class LanguageModelsService implements ILanguageModelsService { } async selectLanguageModels(selector: ILanguageModelChatSelector): Promise { - await this._extensionService.activateByEvent(`onLanguageModelChat:${selector.vendor ?? '*'}}`); + + if (selector.vendor) { + // selective activation + await this._extensionService.activateByEvent(`onLanguageModelChat:${selector.vendor}}`); + } else { + // activate all extensions that do language models + const all = Array.from(this._vendors).map(vendor => this._extensionService.activateByEvent(`onLanguageModelChat:${vendor}`)); + await Promise.all(all); + } const result: string[] = []; for (const model of this._providers.values()) { + if (selector.vendor !== undefined && model.metadata.vendor === selector.vendor || selector.family !== undefined && model.metadata.family === selector.family || selector.version !== undefined && model.metadata.version === selector.version @@ -121,7 +210,12 @@ export class LanguageModelsService implements ILanguageModelsService { // true selection result.push(model.metadata.identifier); - } else if (!selector || isEmptyObject(selector)) { + } else if (!selector || ( + selector.vendor === undefined + && selector.family === undefined + && selector.version === undefined + && selector.identifier === undefined) + ) { // no selection result.push(model.metadata.identifier); } @@ -131,6 +225,11 @@ export class LanguageModelsService implements ILanguageModelsService { } registerLanguageModelChat(identifier: string, provider: ILanguageModelChat): IDisposable { + if (!this._vendors.has(provider.metadata.vendor)) { + // throw new Error(`Chat response provider uses UNKNOWN vendor ${provider.metadata.vendor}.`); + console.warn('USING UNKNOWN vendor', provider.metadata.vendor); + this._vendors.add(provider.metadata.vendor); + } if (this._providers.has(identifier)) { throw new Error(`Chat response provider with identifier ${identifier} is already registered.`); } diff --git a/src/vscode-dts/vscode.proposed.languageModels.d.ts b/src/vscode-dts/vscode.proposed.languageModels.d.ts index df47fd0cec7..86445613f91 100644 --- a/src/vscode-dts/vscode.proposed.languageModels.d.ts +++ b/src/vscode-dts/vscode.proposed.languageModels.d.ts @@ -47,8 +47,6 @@ declare module 'vscode' { role: LanguageModelChatMessageRole.Assistant; content: AsyncIterable; }; - - } /** @@ -95,12 +93,6 @@ declare module 'vscode' { constructor(role: LanguageModelChatMessageRole, content: string, name?: string); } - // --------------------------- - // Language Model Object (V2) - // (+) can pick by id or family - // (++) makes it harder to hardcode an identifier of a model in source code - - // TODO@API name LanguageModelChatEndpoint export interface LanguageModelChat { /** @@ -130,6 +122,8 @@ declare module 'vscode' { // TODO@API // max_prompt_tokens vs output_tokens vs context_size + // readonly inputTokens: number; + // readonly outputTokens: number; readonly contextSize: number; /** @@ -174,9 +168,13 @@ declare module 'vscode' { export interface LanguageModelChatSelector { - vendor?: string; // TODO@API make required? + // TODO@API make required? + vendor?: string; + family?: string; + version?: string; + id?: string; // TODO@API tokens? min/max etc } @@ -250,12 +248,10 @@ declare module 'vscode' { * Select chat models by a {@link LanguageModelChatSelector selector}. This can yield in multiple or no chat models * and extension must handle these cases, esp when no chat model exists. * - * @param selector A chat model selector. + * @param selector A chat model selector. When omitted all chat models are returned. * @returns An array of chat models or `undefined` when no chat model was selected. */ - // (++) lazy activation - // (++) give specific LM to some extension - export function selectChatModels(selector: LanguageModelChatSelector): Thenable; + export function selectChatModels(selector?: LanguageModelChatSelector): Thenable; } /** From 10a9adb2da28c7e627a5c574f45a5cba27f5e2b3 Mon Sep 17 00:00:00 2001 From: Ulugbek Abdullaev Date: Wed, 8 May 2024 16:19:13 +0200 Subject: [PATCH 045/357] runCommands: fix: do not try stringify-ing circular objects --- .../contrib/commands/common/commands.contribution.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/commands/common/commands.contribution.ts b/src/vs/workbench/contrib/commands/common/commands.contribution.ts index c4d4a5b3649..5e5b329cd8e 100644 --- a/src/vs/workbench/contrib/commands/common/commands.contribution.ts +++ b/src/vs/workbench/contrib/commands/common/commands.contribution.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { safeStringify } from 'vs/base/common/objects'; import * as nls from 'vs/nls'; import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { ICommandService } from 'vs/platform/commands/common/commands'; @@ -99,14 +100,14 @@ class RunCommands extends Action2 { const cmd = args.commands[i]; - logService.debug(`runCommands: executing ${i}-th command: ${JSON.stringify(cmd)}`); + logService.debug(`runCommands: executing ${i}-th command: ${safeStringify(cmd)}`); const r = await this._runCommand(commandService, cmd); - logService.debug(`runCommands: executed ${i}-th command with return value: ${JSON.stringify(r)}`); + logService.debug(`runCommands: executed ${i}-th command with return value: ${safeStringify(r)}`); } } catch (err) { - logService.debug(`runCommands: executing ${i}-th command resulted in an error: ${err instanceof Error ? err.message : JSON.stringify(err)}`); + logService.debug(`runCommands: executing ${i}-th command resulted in an error: ${err instanceof Error ? err.message : safeStringify(err)}`); notificationService.error(err); } From 69a80f9f1d4bb74fa5b8db04be8a080328a0fb86 Mon Sep 17 00:00:00 2001 From: Johannes Date: Wed, 8 May 2024 16:48:21 +0200 Subject: [PATCH 046/357] use a composted, semi-random identifier for language model and leave metadata#id for model selection only --- .../api/browser/mainThreadLanguageModels.ts | 16 +-------- .../workbench/api/common/extHost.api.impl.ts | 2 +- .../workbench/api/common/extHost.protocol.ts | 4 +-- .../api/common/extHostLanguageModels.ts | 22 +++++------- .../contrib/chat/common/languageModels.ts | 35 ++++++++++++------- 5 files changed, 35 insertions(+), 44 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadLanguageModels.ts b/src/vs/workbench/api/browser/mainThreadLanguageModels.ts index 964628c863a..8b9ebfcf0dc 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageModels.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageModels.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { coalesce } from 'vs/base/common/arrays'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; @@ -37,8 +36,7 @@ export class MainThreadLanguageModels implements MainThreadLanguageModelsShape { @IExtensionService private readonly _extensionService: IExtensionService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostChatProvider); - - this._proxy.$acceptChatModelMetadata({ added: coalesce(_chatProviderService.getLanguageModelIds().map(id => _chatProviderService.lookupLanguageModel(id))) }); + this._proxy.$acceptChatModelMetadata({ added: _chatProviderService.getLanguageModelIds().map(id => ({ identifier: id, metadata: _chatProviderService.lookupLanguageModel(id)! })) }); this._store.add(_chatProviderService.onDidChangeLanguageModels(this._proxy.$acceptChatModelMetadata, this._proxy)); } @@ -88,18 +86,6 @@ export class MainThreadLanguageModels implements MainThreadLanguageModelsShape { async $prepareChatAccess(extension: ExtensionIdentifier, providerId: string, justification?: string): Promise { - const activate = this._extensionService.activateByEvent(`onLanguageModelAccess:${providerId}`); - const metadata = this._chatProviderService.lookupLanguageModel(providerId); - - if (metadata) { - return metadata; - } - - await Promise.race([ - activate, - Event.toPromise(Event.filter(this._chatProviderService.onDidChangeLanguageModels, e => Boolean(e.added?.some(value => value.identifier === providerId)))) - ]); - return this._chatProviderService.lookupLanguageModel(providerId); } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index f5d4f04d280..d0009f2ec1f 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1430,7 +1430,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const lm: typeof vscode.lm = { selectChatModels: (selector) => { checkProposedApiEnabled(extension, 'languageModels'); - return extHostLanguageModels.selectLanguageModels(extension, selector); + return extHostLanguageModels.selectLanguageModels(extension, selector ?? {}); }, onDidChangeChatModels: (listener, thisArgs?, disposables?) => { checkProposedApiEnabled(extension, 'languageModels'); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index e4a7a398823..130ffae9d6e 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -54,7 +54,7 @@ import { ChatAgentLocation, IChatAgentMetadata, IChatAgentRequest, IChatAgentRes import { IChatProgressResponseContent } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChatFollowup, IChatProgress, IChatResponseErrorDetails, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatRequestVariableValue, IChatVariableData, IChatVariableResolverProgress } from 'vs/workbench/contrib/chat/common/chatVariables'; -import { IChatMessage, IChatResponseFragment, ILanguageModelChatMetadata, ILanguageModelChatSelector } from 'vs/workbench/contrib/chat/common/languageModels'; +import { IChatMessage, IChatResponseFragment, ILanguageModelChatMetadata, ILanguageModelChatSelector, ILanguageModelsChangeEvent } from 'vs/workbench/contrib/chat/common/languageModels'; import { DebugConfigurationProviderTriggerKind, IAdapterDescriptor, IConfig, IDebugSessionReplMode, IDebugVisualization, IDebugVisualizationContext, IDebugVisualizationTreeItem, MainThreadDebugVisualization } from 'vs/workbench/contrib/debug/common/debug'; import * as notebookCommon from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CellExecutionUpdateType } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; @@ -1211,7 +1211,7 @@ export interface MainThreadLanguageModelsShape extends IDisposable { } export interface ExtHostLanguageModelsShape { - $acceptChatModelMetadata(data: { added?: ILanguageModelChatMetadata[]; removed?: string[] }): void; + $acceptChatModelMetadata(data: ILanguageModelsChangeEvent): void; $updateModelAccesslist(data: { from: ExtensionIdentifier; to: ExtensionIdentifier; enabled: boolean }[]): void; $provideLanguageModelResponse(handle: number, requestId: number, from: ExtensionIdentifier, messages: IChatMessage[], options: { [name: string]: any }, token: CancellationToken): Promise; $handleResponseFragment(requestId: number, chunk: IChatResponseFragment): Promise; diff --git a/src/vs/workbench/api/common/extHostLanguageModels.ts b/src/vs/workbench/api/common/extHostLanguageModels.ts index d19e5ee4d99..53351f21ca5 100644 --- a/src/vs/workbench/api/common/extHostLanguageModels.ts +++ b/src/vs/workbench/api/common/extHostLanguageModels.ts @@ -152,9 +152,9 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { accountLabel: typeof metadata.auth === 'object' ? metadata.auth.label : undefined }; } - this._proxy.$registerLanguageModelProvider(handle, identifier, { + this._proxy.$registerLanguageModelProvider(handle, `${ExtensionIdentifier.toKey(extension.identifier)}/${handle}/${identifier}`, { extension: extension.identifier, - identifier: identifier, + id: identifier, vendor: metadata.vendor ?? ExtensionIdentifier.toKey(extension.identifier), name: metadata.name ?? '', family: metadata.family ?? '', @@ -211,20 +211,16 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { //#region --- making request - $acceptChatModelMetadata(data: { added?: ILanguageModelChatMetadata[] | undefined; removed?: string[] | undefined }): void { - const added: string[] = []; - const removed: string[] = []; + $acceptChatModelMetadata(data: { added?: { identifier: string; metadata: ILanguageModelChatMetadata }[] | undefined; removed?: string[] | undefined }): void { if (data.added) { - for (const metadata of data.added) { - this._allLanguageModelData.set(metadata.identifier, { metadata, apiObjects: new ExtensionIdentifierMap() }); - added.push(metadata.identifier); + for (const { identifier, metadata } of data.added) { + this._allLanguageModelData.set(identifier, { metadata, apiObjects: new ExtensionIdentifierMap() }); } } if (data.removed) { for (const id of data.removed) { // clean up this._allLanguageModelData.delete(id); - removed.push(id); // cancel pending requests for this model for (const [key, value] of this._pendingRequest) { @@ -236,10 +232,10 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { } } - this._onDidChangeProviders.fire(undefined); - // TODO@jrieken@TylerLeonhardt - this is a temporary hack to populate the auth providers - data.added?.forEach(this._fakeAuthPopulate, this); + data.added?.forEach(added => this._fakeAuthPopulate(added.metadata)); + + this._onDidChangeProviders.fire(undefined); } async selectLanguageModels(extension: IExtensionDescription, selector: vscode.LanguageModelChatSelector) { @@ -433,7 +429,7 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { return local.provider.provideTokenCount(value, token); } - return this._proxy.$countTokens(data.metadata.identifier, (typeof value === 'string' ? value : typeConvert.LanguageModelChatMessage.from(value)), token); + return this._proxy.$countTokens(languageModelId, (typeof value === 'string' ? value : typeConvert.LanguageModelChatMessage.from(value)), token); } $updateModelAccesslist(data: { from: ExtensionIdentifier; to: ExtensionIdentifier; enabled: boolean }[]): void { diff --git a/src/vs/workbench/contrib/chat/common/languageModels.ts b/src/vs/workbench/contrib/chat/common/languageModels.ts index b5b90dcf1c5..d0021ad3e9f 100644 --- a/src/vs/workbench/contrib/chat/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/common/languageModels.ts @@ -34,8 +34,9 @@ export interface IChatResponseFragment { export interface ILanguageModelChatMetadata { readonly extension: ExtensionIdentifier; + readonly name: string; - readonly identifier: string; + readonly id: string; readonly vendor: string; readonly version: string; readonly family: string; @@ -66,11 +67,19 @@ export interface ILanguageModelChatSelector { export const ILanguageModelsService = createDecorator('ILanguageModelsService'); +export interface ILanguageModelsChangeEvent { + added?: { + identifier: string; + metadata: ILanguageModelChatMetadata; + }[]; + removed?: string[]; +} + export interface ILanguageModelsService { readonly _serviceBrand: undefined; - onDidChangeLanguageModels: Event<{ added?: ILanguageModelChatMetadata[]; removed?: string[] }>; + onDidChangeLanguageModels: Event; getLanguageModelIds(): string[]; @@ -113,7 +122,7 @@ export const languageModelExtensionPoint = ExtensionsRegistry.registerExtensionP }, activationEventsGenerator: (contribs: IUserFriendlyLanguageModel[], result: { push(item: string): void }) => { for (const contrib of contribs) { - result.push(`onLanguageModel:${contrib.vendor}`); + result.push(`onLanguageModelChat:${contrib.vendor}`); } } }); @@ -125,8 +134,8 @@ export class LanguageModelsService implements ILanguageModelsService { private readonly _providers = new Map(); private readonly _vendors = new Set(); - private readonly _onDidChangeProviders = new Emitter<{ added?: ILanguageModelChatMetadata[]; removed?: string[] }>(); - readonly onDidChangeLanguageModels: Event<{ added?: ILanguageModelChatMetadata[]; removed?: string[] }> = this._onDidChangeProviders.event; + private readonly _onDidChangeProviders = new Emitter(); + readonly onDidChangeLanguageModels: Event = this._onDidChangeProviders.event; constructor( @IExtensionService private readonly _extensionService: IExtensionService, @@ -161,10 +170,10 @@ export class LanguageModelsService implements ILanguageModelsService { } const removed: string[] = []; - for (const [key, value] of this._providers) { + for (const [identifier, value] of this._providers) { if (!this._vendors.has(value.metadata.vendor)) { - this._providers.delete(key); - removed.push(key); + this._providers.delete(identifier); + removed.push(identifier); } } if (removed.length > 0) { @@ -199,16 +208,16 @@ export class LanguageModelsService implements ILanguageModelsService { const result: string[] = []; - for (const model of this._providers.values()) { + for (const [identifier, model] of this._providers) { if (selector.vendor !== undefined && model.metadata.vendor === selector.vendor || selector.family !== undefined && model.metadata.family === selector.family || selector.version !== undefined && model.metadata.version === selector.version - || selector.identifier !== undefined && model.metadata.identifier === selector.identifier + || selector.identifier !== undefined && model.metadata.id === selector.identifier || selector.extension !== undefined && model.metadata.targetExtensions?.some(candidate => ExtensionIdentifier.equals(candidate, selector.extension)) ) { // true selection - result.push(model.metadata.identifier); + result.push(identifier); } else if (!selector || ( selector.vendor === undefined @@ -217,7 +226,7 @@ export class LanguageModelsService implements ILanguageModelsService { && selector.identifier === undefined) ) { // no selection - result.push(model.metadata.identifier); + result.push(identifier); } } @@ -234,7 +243,7 @@ export class LanguageModelsService implements ILanguageModelsService { throw new Error(`Chat response provider with identifier ${identifier} is already registered.`); } this._providers.set(identifier, provider); - this._onDidChangeProviders.fire({ added: [provider.metadata] }); + this._onDidChangeProviders.fire({ added: [{ identifier, metadata: provider.metadata }] }); return toDisposable(() => { if (this._providers.delete(identifier)) { this._onDidChangeProviders.fire({ removed: [identifier] }); From 0486d991a82613d8d1ce9b359fb4c3dccab1d63e Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 8 May 2024 16:57:18 +0200 Subject: [PATCH 047/357] feat: add conditional expressions to keybindings and actions, update dictation start action conditions (#212264) --- src/vs/platform/action/common/action.ts | 4 + src/vs/platform/actions/common/actions.ts | 3 + .../keybinding/common/keybindingsRegistry.ts | 3 + .../actions/voiceChatActions.ts | 107 +++++++++++------- .../browser/dictation/editorDictation.ts | 8 +- .../contrib/speech/browser/speechService.ts | 3 - 6 files changed, 85 insertions(+), 43 deletions(-) diff --git a/src/vs/platform/action/common/action.ts b/src/vs/platform/action/common/action.ts index 8b67550ed21..c0f4c2dd053 100644 --- a/src/vs/platform/action/common/action.ts +++ b/src/vs/platform/action/common/action.ts @@ -85,6 +85,10 @@ export interface ICommandAction { tooltip?: string | ILocalizedString; icon?: Icon; source?: ICommandActionSource; + /** + * Precondition controls enablement (for example for a menu item, show + * it in grey or for a command, do not allow to invoke it) + */ precondition?: ContextKeyExpression; /** diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index e97213f2f99..fa9bd76ed8a 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -18,6 +18,9 @@ import { IKeybindingRule, KeybindingsRegistry } from 'vs/platform/keybinding/com export interface IMenuItem { command: ICommandAction; alt?: ICommandAction; + /** + * Menu item is hidden if this expression returns false. + */ when?: ContextKeyExpression; group?: 'navigation' | string; order?: number; diff --git a/src/vs/platform/keybinding/common/keybindingsRegistry.ts b/src/vs/platform/keybinding/common/keybindingsRegistry.ts index 4ab820a3732..6c88451dff6 100644 --- a/src/vs/platform/keybinding/common/keybindingsRegistry.ts +++ b/src/vs/platform/keybinding/common/keybindingsRegistry.ts @@ -43,6 +43,9 @@ export interface IKeybindingRule extends IKeybindings { id: string; weight: number; args?: any; + /** + * Keybinding is disabled if expression returns false. + */ when?: ContextKeyExpression | null | undefined; } diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts index 999c93c92cd..36cc1487c93 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts @@ -12,7 +12,7 @@ import { Event } from 'vs/base/common/event'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ThemeIcon } from 'vs/base/common/themables'; -import { assertIsDefined, isNumber } from 'vs/base/common/types'; +import { isNumber } from 'vs/base/common/types'; import { getCodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { localize, localize2 } from 'vs/nls'; @@ -46,7 +46,7 @@ import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/com import { InlineChatController } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; -import { HasSpeechProvider, ISpeechService, KeywordRecognitionStatus, SpeechToTextStatus, SpeechToTextInProgress, TextToSpeechInProgress } from 'vs/workbench/contrib/speech/common/speechService'; +import { HasSpeechProvider, ISpeechService, KeywordRecognitionStatus, SpeechToTextInProgress, SpeechToTextStatus, TextToSpeechInProgress } from 'vs/workbench/contrib/speech/common/speechService'; import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalChatContextKeys, TerminalChatController } from 'vs/workbench/contrib/terminal/browser/terminalContribExports'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -67,7 +67,9 @@ const CONTEXT_VOICE_CHAT_IN_VIEW_IN_PROGRESS = new RawContextKey('voice const CONTEXT_VOICE_CHAT_IN_EDITOR_IN_PROGRESS = new RawContextKey('voiceChatInEditorInProgress', false, { type: 'boolean', description: localize('voiceChatInEditorInProgress', "True when voice recording from microphone is in progress in the chat editor.") }); const CanVoiceChat = ContextKeyExpr.and(CONTEXT_CHAT_ENABLED, HasSpeechProvider); -const FocusInChatInput = assertIsDefined(ContextKeyExpr.or(CTX_INLINE_CHAT_FOCUSED, CONTEXT_IN_CHAT_INPUT)); +const FocusInChatInput = ContextKeyExpr.or(CTX_INLINE_CHAT_FOCUSED, CONTEXT_IN_CHAT_INPUT); + +const AnyChatRequestInProgress = ContextKeyExpr.or(CONTEXT_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST, TerminalChatContextKeys.requestActive); type VoiceChatSessionContext = 'inline' | 'terminal' | 'quick' | 'view' | 'editor'; @@ -92,10 +94,10 @@ class VoiceChatSessionControllerFactory { static create(accessor: ServicesAccessor, context: 'inline'): Promise; static create(accessor: ServicesAccessor, context: 'quick'): Promise; static create(accessor: ServicesAccessor, context: 'view'): Promise; - static create(accessor: ServicesAccessor, context: 'focused'): Promise; static create(accessor: ServicesAccessor, context: 'terminal'): Promise; - static create(accessor: ServicesAccessor, context: 'inline' | 'terminal' | 'quick' | 'view' | 'focused'): Promise; - static async create(accessor: ServicesAccessor, context: 'inline' | 'terminal' | 'quick' | 'view' | 'focused'): Promise { + static create(accessor: ServicesAccessor, context: 'focused'): Promise; + static create(accessor: ServicesAccessor, context: 'inline' | 'quick' | 'view' | 'terminal' | 'focused'): Promise; + static async create(accessor: ServicesAccessor, context: 'inline' | 'quick' | 'view' | 'terminal' | 'focused'): Promise { const chatWidgetService = accessor.get(IChatWidgetService); const viewsService = accessor.get(IViewsService); const quickChatService = accessor.get(IQuickChatService); @@ -310,11 +312,15 @@ class VoiceChatSessions { constructor( @IContextKeyService private readonly contextKeyService: IContextKeyService, @IVoiceChatService private readonly voiceChatService: IVoiceChatService, - @IConfigurationService private readonly configurationService: IConfigurationService + @IConfigurationService private readonly configurationService: IConfigurationService, + @IInstantiationService private readonly instantiationService: IInstantiationService ) { } async start(controller: IVoiceChatSessionController, context?: IChatExecuteActionContext): Promise { + + // Stop running text-to-speech or speech-to-text sessions in chats this.stop(); + ChatSynthesizerSessions.getInstance(this.instantiationService).stop(); let disableTimeout = false; @@ -501,7 +507,10 @@ export class VoiceChatInChatViewAction extends VoiceChatWithHoldModeAction { id: VoiceChatInChatViewAction.ID, title: localize2('workbench.action.chat.voiceChatInView.label', "Voice Chat in View"), category: CHAT_CATEGORY, - precondition: ContextKeyExpr.and(CanVoiceChat, CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate()), + precondition: ContextKeyExpr.and( + CanVoiceChat, + CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate() // disable when a chat request is in progress + ), f1: true }, 'view'); } @@ -519,9 +528,10 @@ export class HoldToVoiceChatInChatViewAction extends Action2 { weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and( CanVoiceChat, - FocusInChatInput.negate(), // when already in chat input, disable this action and prefer to start voice chat directly - EditorContextKeys.focus.negate(), // do not steal the inline-chat keybinding - NOTEBOOK_EDITOR_FOCUSED.negate() // do not steal the notebook keybinding + CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate(), // disable when a chat request is in progress + FocusInChatInput?.negate(), // when already in chat input, disable this action and prefer to start voice chat directly + EditorContextKeys.focus.negate(), // do not steal the inline-chat keybinding + NOTEBOOK_EDITOR_FOCUSED.negate() // do not steal the notebook keybinding ), primary: KeyMod.CtrlCmd | KeyCode.KeyI } @@ -568,7 +578,11 @@ export class InlineVoiceChatAction extends VoiceChatWithHoldModeAction { id: InlineVoiceChatAction.ID, title: localize2('workbench.action.chat.inlineVoiceChat', "Inline Voice Chat"), category: CHAT_CATEGORY, - precondition: ContextKeyExpr.and(CanVoiceChat, ActiveEditorContext, CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate()), + precondition: ContextKeyExpr.and( + CanVoiceChat, + ActiveEditorContext, + CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate() // disable when a chat request is in progress + ), f1: true }, 'inline'); } @@ -583,7 +597,10 @@ export class QuickVoiceChatAction extends VoiceChatWithHoldModeAction { id: QuickVoiceChatAction.ID, title: localize2('workbench.action.chat.quickVoiceChat.label', "Quick Voice Chat"), category: CHAT_CATEGORY, - precondition: ContextKeyExpr.and(CanVoiceChat, CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate()), + precondition: ContextKeyExpr.and( + CanVoiceChat, + CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate() // disable when a chat request is in progress + ), f1: true }, 'quick'); } @@ -602,27 +619,37 @@ export class StartVoiceChatAction extends Action2 { keybinding: { weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and( - FocusInChatInput, // scope this action to chat input fields only - EditorContextKeys.focus.negate(), // do not steal the inline-chat keybinding - NOTEBOOK_EDITOR_FOCUSED.negate(), // do not steal the notebook keybinding - CONTEXT_VOICE_CHAT_IN_VIEW_IN_PROGRESS.negate(), - CONTEXT_QUICK_VOICE_CHAT_IN_PROGRESS.negate(), - CONTEXT_VOICE_CHAT_IN_EDITOR_IN_PROGRESS.negate(), - CONTEXT_INLINE_VOICE_CHAT_IN_PROGRESS.negate(), - CONTEXT_TERMINAL_VOICE_CHAT_IN_PROGRESS.negate() + FocusInChatInput, // scope this action to chat input fields only + EditorContextKeys.focus.negate(), // do not steal the editor inline-chat keybinding + NOTEBOOK_EDITOR_FOCUSED.negate() // do not steal the notebook inline-chat keybinding ), primary: KeyMod.CtrlCmd | KeyCode.KeyI }, icon: Codicon.mic, - precondition: ContextKeyExpr.and(CanVoiceChat, CONTEXT_VOICE_CHAT_GETTING_READY.negate(), CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate(), CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST.negate(), TerminalChatContextKeys.requestActive.negate()), + precondition: ContextKeyExpr.and( + CanVoiceChat, + CONTEXT_VOICE_CHAT_GETTING_READY.negate(), // disable when voice chat is getting ready + AnyChatRequestInProgress?.negate(), // disable when any chat request is in progress + SpeechToTextInProgress.negate() // disable when speech to text is in progress + ), menu: [{ id: MenuId.ChatExecute, - when: ContextKeyExpr.and(HasSpeechProvider, CONTEXT_VOICE_CHAT_IN_VIEW_IN_PROGRESS.negate(), CONTEXT_QUICK_VOICE_CHAT_IN_PROGRESS.negate(), CONTEXT_VOICE_CHAT_IN_EDITOR_IN_PROGRESS.negate(), TextToSpeechInProgress.negate()), + when: ContextKeyExpr.and( + HasSpeechProvider, + TextToSpeechInProgress.negate(), // hide when text to speech is in progress + CONTEXT_VOICE_CHAT_IN_VIEW_IN_PROGRESS.negate(), // hide when voice chat is in progress + CONTEXT_QUICK_VOICE_CHAT_IN_PROGRESS.negate(), // || + CONTEXT_VOICE_CHAT_IN_EDITOR_IN_PROGRESS.negate(), // || + ), group: 'navigation', order: -1 }, { id: MenuId.for('terminalChatInput'), - when: ContextKeyExpr.and(HasSpeechProvider, CONTEXT_TERMINAL_VOICE_CHAT_IN_PROGRESS.negate(), TextToSpeechInProgress.negate()), + when: ContextKeyExpr.and( + HasSpeechProvider, + TextToSpeechInProgress.negate(), // hide when text to speech is in progress + CONTEXT_TERMINAL_VOICE_CHAT_IN_PROGRESS.negate(), // hide when voice chat is in progress + ), group: 'navigation', order: -1 }] @@ -794,25 +821,29 @@ export class StopListeningAndSubmitAction extends Action2 { //#region Text to Speech -class TextToSpeechSessions { +class ChatSynthesizerSessions { - private static instance: TextToSpeechSessions | undefined = undefined; - static getInstance(instantiationService: IInstantiationService): TextToSpeechSessions { - if (!TextToSpeechSessions.instance) { - TextToSpeechSessions.instance = instantiationService.createInstance(TextToSpeechSessions); + private static instance: ChatSynthesizerSessions | undefined = undefined; + static getInstance(instantiationService: IInstantiationService): ChatSynthesizerSessions { + if (!ChatSynthesizerSessions.instance) { + ChatSynthesizerSessions.instance = instantiationService.createInstance(ChatSynthesizerSessions); } - return TextToSpeechSessions.instance; + return ChatSynthesizerSessions.instance; } private activeSession: CancellationTokenSource | undefined = undefined; constructor( - @ISpeechService private readonly speechService: ISpeechService + @ISpeechService private readonly speechService: ISpeechService, + @IInstantiationService private readonly instantiationService: IInstantiationService ) { } async start(text: string): Promise { + + // Stop running text-to-speech or speech-to-text sessions in chats this.stop(); + VoiceChatSessions.getInstance(this.instantiationService).stop(); const activeSession = this.activeSession = new CancellationTokenSource(); @@ -833,10 +864,10 @@ export class ReadChatItemAloud extends Action2 { title: localize2('workbench.action.chat.readChatItemAloud', "Read Aloud"), f1: false, category: CHAT_CATEGORY, - precondition: ContextKeyExpr.and(CanVoiceChat, SpeechToTextInProgress.toNegated()), + precondition: CanVoiceChat, menu: { id: MenuId.ChatContext, - when: ContextKeyExpr.and(CanVoiceChat, CONTEXT_RESPONSE_FILTERED.toNegated()), + when: ContextKeyExpr.and(CanVoiceChat, CONTEXT_RESPONSE_FILTERED.negate()), group: 'textToSpeech' } }); @@ -848,7 +879,7 @@ export class ReadChatItemAloud extends Action2 { return; } - TextToSpeechSessions.getInstance(accessor.get(IInstantiationService)).start(stringifyItem(item, false)); + ChatSynthesizerSessions.getInstance(accessor.get(IInstantiationService)).start(stringifyItem(item, false)); } } @@ -866,15 +897,15 @@ export class StopReadAloud extends Action2 { precondition: TextToSpeechInProgress, keybinding: { weight: KeybindingWeight.WorkbenchContrib + 100, - primary: KeyCode.Escape + primary: KeyCode.Escape, }, menu: [{ id: MenuId.ChatContext, - when: ContextKeyExpr.and(CanVoiceChat, TextToSpeechInProgress), + when: TextToSpeechInProgress, group: 'textToSpeech' }, { id: MenuId.ChatExecute, - when: ContextKeyExpr.and(CanVoiceChat, TextToSpeechInProgress), + when: TextToSpeechInProgress, group: 'navigation', order: -1 }] @@ -882,7 +913,7 @@ export class StopReadAloud extends Action2 { } async run(accessor: ServicesAccessor, ...args: any[]) { - TextToSpeechSessions.getInstance(accessor.get(IInstantiationService)).stop(); + ChatSynthesizerSessions.getInstance(accessor.get(IInstantiationService)).stop(); } } diff --git a/src/vs/workbench/contrib/codeEditor/browser/dictation/editorDictation.ts b/src/vs/workbench/contrib/codeEditor/browser/dictation/editorDictation.ts index 377f64adf0a..705308f8619 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/dictation/editorDictation.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/dictation/editorDictation.ts @@ -11,7 +11,7 @@ import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { ContextKeyExpr, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { HasSpeechProvider, ISpeechService, SpeechToTextStatus } from 'vs/workbench/contrib/speech/common/speechService'; +import { HasSpeechProvider, ISpeechService, SpeechToTextInProgress, SpeechToTextStatus } from 'vs/workbench/contrib/speech/common/speechService'; import { Codicon } from 'vs/base/common/codicons'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { EditorAction2, EditorContributionInstantiation, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; @@ -40,7 +40,11 @@ export class EditorDictationStartAction extends EditorAction2 { id: 'workbench.action.editorDictation.start', title: localize2('startDictation', "Start Dictation in Editor"), category: VOICE_CATEGORY, - precondition: ContextKeyExpr.and(HasSpeechProvider, EDITOR_DICTATION_IN_PROGRESS.toNegated(), EditorContextKeys.readOnly.toNegated()), + precondition: ContextKeyExpr.and( + HasSpeechProvider, + SpeechToTextInProgress.toNegated(), // disable when any speech-to-text is in progress + EditorContextKeys.readOnly.toNegated() // disable in read-only editors + ), f1: true, keybinding: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyV, diff --git a/src/vs/workbench/contrib/speech/browser/speechService.ts b/src/vs/workbench/contrib/speech/browser/speechService.ts index 7dc6c571132..b0794137e25 100644 --- a/src/vs/workbench/contrib/speech/browser/speechService.ts +++ b/src/vs/workbench/contrib/speech/browser/speechService.ts @@ -333,9 +333,6 @@ export class SpeechService extends Disposable implements ISpeechService { async recognizeKeyword(token: CancellationToken): Promise { const result = new DeferredPromise(); - // Send out extension activation to ensure providers can register - await this.extensionService.activateByEvent('onSpeech'); - const disposables = new DisposableStore(); disposables.add(token.onCancellationRequested(() => { disposables.dispose(); From 6e24e5f2bdf801a3b70f6c3068073e4af8dc6a40 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 8 May 2024 08:10:08 -0700 Subject: [PATCH 048/357] fix compile error --- src/vs/workbench/api/common/extHost.api.impl.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index d0b5accb676..6642a348f93 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1683,6 +1683,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I DataTransferItem: extHostTypes.DataTransferItem, TestCoverageCount: extHostTypes.TestCoverageCount, FileCoverage: extHostTypes.FileCoverage, + FileCoverage2: extHostTypes.FileCoverage, StatementCoverage: extHostTypes.StatementCoverage, BranchCoverage: extHostTypes.BranchCoverage, DeclarationCoverage: extHostTypes.DeclarationCoverage, From 89ee8b8cc697027a1efd2d4c2a4e9cd095268c33 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 8 May 2024 18:19:30 +0200 Subject: [PATCH 049/357] add logging (#212268) --- .../node/extensionSignatureVerificationService.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/platform/extensionManagement/node/extensionSignatureVerificationService.ts b/src/vs/platform/extensionManagement/node/extensionSignatureVerificationService.ts index a835fb53111..a76ca930005 100644 --- a/src/vs/platform/extensionManagement/node/extensionSignatureVerificationService.ts +++ b/src/vs/platform/extensionManagement/node/extensionSignatureVerificationService.ts @@ -121,6 +121,7 @@ export class ExtensionSignatureVerificationService implements IExtensionSignatur let result: ExtensionSignatureVerificationResult; try { + this.logService.trace(`Verifying extension signature for ${extensionId}...`); result = await module.verify(vsixFilePath, signatureArchiveFilePath, this.logService.getLevel() === LogLevel.Trace); } catch (e) { result = { @@ -132,7 +133,7 @@ export class ExtensionSignatureVerificationService implements IExtensionSignatur const duration = new Date().getTime() - startTime; - this.logService.info(`Extension signature verification result for ${extensionId}: ${result.code}. Duration: ${duration}ms.`); + this.logService.info(`Extension signature verification result for ${extensionId}: ${result.code}. Executed: ${result.didExecute}. Duration: ${duration}ms.`); this.logService.trace(`Extension signature verification output for ${extensionId}:\n${result.output}`); type ExtensionSignatureVerificationClassification = { From 349dc8b92e3d81b095a5a2fc62085cf7b04af7fb Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 8 May 2024 19:11:38 +0200 Subject: [PATCH 050/357] use random file name (#212269) --- .../platform/extensionManagement/node/extensionDownloader.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/platform/extensionManagement/node/extensionDownloader.ts b/src/vs/platform/extensionManagement/node/extensionDownloader.ts index 0a96fee799d..d670f821602 100644 --- a/src/vs/platform/extensionManagement/node/extensionDownloader.ts +++ b/src/vs/platform/extensionManagement/node/extensionDownloader.ts @@ -111,9 +111,9 @@ export class ExtensionsDownloader extends Disposable { private async downloadSignatureArchive(extension: IGalleryExtension): Promise { await this.cleanUpPromise; - const location = joinPath(this.extensionsDownloadDir, `${this.getName(extension)}${ExtensionsDownloader.SignatureArchiveExtension}`); + const location = joinPath(this.extensionsDownloadDir, `.${generateUuid()}`); try { - await this.downloadFile(extension, location, location => this.extensionGalleryService.downloadSignatureArchive(extension, location)); + await this.extensionGalleryService.downloadSignatureArchive(extension, location); } catch (error) { throw toExtensionManagementError(error, ExtensionManagementErrorCode.DownloadSignature); } From b2252bdb4a3d97631695f6cd5260e56bb94990e0 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 8 May 2024 19:33:48 +0200 Subject: [PATCH 051/357] some more jsdoc (#212270) --- .../vscode.proposed.languageModels.d.ts | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/vscode-dts/vscode.proposed.languageModels.d.ts b/src/vscode-dts/vscode.proposed.languageModels.d.ts index 86445613f91..1e4782e366d 100644 --- a/src/vscode-dts/vscode.proposed.languageModels.d.ts +++ b/src/vscode-dts/vscode.proposed.languageModels.d.ts @@ -166,16 +166,37 @@ declare module 'vscode' { computeTokenLength(text: string | LanguageModelChatMessage, token?: CancellationToken): Thenable; } - + /** + * Describes how to select language models for chat requests. + * + * @see {@link lm.selectChatModels} + */ export interface LanguageModelChatSelector { - // TODO@API make required? + + /** + * A vendor of language models. + * @see {@link LanguageModelChat.vendor} + */ vendor?: string; + /** + * A family of language models. + * @see {@link LanguageModelChat.family} + */ family?: string; + /** + * The version of a language model. + * @see {@link LanguageModelChat.version} + */ version?: string; + /** + * The identifier of a language model. + * @see {@link LanguageModelChat.id} + */ id?: string; + // TODO@API tokens? min/max etc } From d3edb5833734cd21448b0b58d0fe3e8ea280156c Mon Sep 17 00:00:00 2001 From: David Dossett Date: Wed, 8 May 2024 10:34:59 -0700 Subject: [PATCH 052/357] Fix p styling in confirmation --- src/vs/workbench/contrib/chat/browser/media/chat.css | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 5d543466f41..414e836d638 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -722,7 +722,7 @@ border: 1px solid var(--vscode-chat-requestBorder); border-radius: 6px; margin-bottom: 16px; - padding: 10px 16px 12px; + padding: 12px 16px 16px; } .chat-confirmation-widget .chat-confirmation-widget-title { @@ -733,6 +733,10 @@ margin: 0 0 4px 0; } +.chat-confirmation-widget .chat-confirmation-widget-message .rendered-markdown p { + margin-top: 0; +} + .chat-confirmation-widget .chat-confirmation-buttons-container { display: flex; gap: 8px; From 6cb778ae8eedb871faee47818dbf01fdbd5e9d01 Mon Sep 17 00:00:00 2001 From: David Dossett Date: Wed, 8 May 2024 10:37:48 -0700 Subject: [PATCH 053/357] Align border radius to other widgets --- src/vs/workbench/contrib/chat/browser/media/chat.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 414e836d638..e52597277ed 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -720,7 +720,7 @@ .chat-confirmation-widget { border: 1px solid var(--vscode-chat-requestBorder); - border-radius: 6px; + border-radius: 4px; margin-bottom: 16px; padding: 12px 16px 16px; } From 91c5a5da9d74bbb4f60b7ccc6c4402d8c65fb673 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 8 May 2024 19:38:33 +0200 Subject: [PATCH 054/357] chore - remove unneccessary RPC call (#212273) --- src/vs/workbench/api/browser/mainThreadLanguageModels.ts | 5 ----- src/vs/workbench/api/common/extHost.protocol.ts | 1 - src/vs/workbench/api/common/extHostLanguageModels.ts | 2 +- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadLanguageModels.ts b/src/vs/workbench/api/browser/mainThreadLanguageModels.ts index 8b9ebfcf0dc..9b14928a514 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageModels.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageModels.ts @@ -84,11 +84,6 @@ export class MainThreadLanguageModels implements MainThreadLanguageModelsShape { this._languageModelStatsService.update(identifier, extensionId, participant, tokenCount); } - async $prepareChatAccess(extension: ExtensionIdentifier, providerId: string, justification?: string): Promise { - - return this._chatProviderService.lookupLanguageModel(providerId); - } - async $fetchResponse(extension: ExtensionIdentifier, providerId: string, requestId: number, messages: IChatMessage[], options: {}, token: CancellationToken): Promise { this._logService.debug('[CHAT] extension request STARTED', extension.value, requestId); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 130ffae9d6e..2dd52e38465 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1203,7 +1203,6 @@ export interface MainThreadLanguageModelsShape extends IDisposable { $selectChatModels(selector: ILanguageModelChatSelector): Promise; - $prepareChatAccess(extension: ExtensionIdentifier, providerId: string, justification?: string): Promise; $fetchResponse(extension: ExtensionIdentifier, provider: string, requestId: number, messages: IChatMessage[], options: {}, token: CancellationToken): Promise; $whenLanguageModelChatRequestMade(identifier: string, extension: ExtensionIdentifier, participant?: string, tokenCount?: number): void; diff --git a/src/vs/workbench/api/common/extHostLanguageModels.ts b/src/vs/workbench/api/common/extHostLanguageModels.ts index 53351f21ca5..0bb8bbf1a52 100644 --- a/src/vs/workbench/api/common/extHostLanguageModels.ts +++ b/src/vs/workbench/api/common/extHostLanguageModels.ts @@ -295,7 +295,7 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { const internalMessages: IChatMessage[] = this._convertMessages(extension, messages); const from = extension.identifier; - const metadata = await this._proxy.$prepareChatAccess(from, languageModelId, options.justification); + const metadata = this._allLanguageModelData.get(languageModelId)?.metadata; if (!metadata || !this._allLanguageModelData.has(languageModelId)) { throw extHostTypes.LanguageModelError.NotFound(`Language model '${languageModelId}' is unknown.`); From b0c5f83a75ce4452df2c7f1ec7e7176398adb9de Mon Sep 17 00:00:00 2001 From: OccasionalDebugger <168763641+OccasionalDebugger@users.noreply.github.com> Date: Wed, 8 May 2024 11:50:47 -0600 Subject: [PATCH 055/357] Pass full function breakpoint options from plugin (#211895) * Pass full function breakpoint options from plugin * Send newly added function breakpoints to DA --- .../workbench/api/browser/mainThreadDebugService.ts | 9 ++++++++- .../workbench/contrib/debug/browser/debugService.ts | 12 +++++++++--- src/vs/workbench/contrib/debug/common/debug.ts | 4 ++-- src/vs/workbench/contrib/debug/common/debugModel.ts | 4 ++-- .../contrib/debug/test/browser/breakpoints.test.ts | 6 +++--- 5 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadDebugService.ts b/src/vs/workbench/api/browser/mainThreadDebugService.ts index c5fe0f3c943..f58ba4c47fb 100644 --- a/src/vs/workbench/api/browser/mainThreadDebugService.ts +++ b/src/vs/workbench/api/browser/mainThreadDebugService.ts @@ -223,7 +223,14 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb })); this.debugService.addBreakpoints(uri.revive(dto.uri), rawbps); } else if (dto.type === 'function') { - this.debugService.addFunctionBreakpoint(dto.functionName, dto.id, dto.mode); + this.debugService.addFunctionBreakpoint({ + name: dto.functionName, + mode: dto.mode, + condition: dto.condition, + hitCondition: dto.hitCondition, + enabled: dto.enabled, + logMessage: dto.logMessage + }, dto.id); } else if (dto.type === 'data') { this.debugService.addDataBreakpoint({ description: dto.label, diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index c5db527c526..90f55a2868d 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -42,7 +42,7 @@ import { DebugSession } from 'vs/workbench/contrib/debug/browser/debugSession'; import { DebugTaskRunner, TaskRunResult } from 'vs/workbench/contrib/debug/browser/debugTaskRunner'; import { CALLSTACK_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, CONTEXT_DEBUG_STATE, CONTEXT_DEBUG_TYPE, CONTEXT_DEBUG_UX, CONTEXT_DISASSEMBLY_VIEW_FOCUS, CONTEXT_HAS_DEBUGGED, CONTEXT_IN_DEBUG_MODE, DEBUG_MEMORY_SCHEME, DEBUG_SCHEME, IAdapterManager, IBreakpoint, IBreakpointData, IBreakpointUpdateData, ICompound, IConfig, IConfigurationManager, IDebugConfiguration, IDebugModel, IDebugService, IDebugSession, IDebugSessionOptions, IEnablement, IExceptionBreakpoint, IGlobalConfig, ILaunch, IStackFrame, IThread, IViewModel, REPL_VIEW_ID, State, VIEWLET_ID, debuggerDisabledMessage, getStateLabel } from 'vs/workbench/contrib/debug/common/debug'; import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; -import { Breakpoint, DataBreakpoint, DebugModel, FunctionBreakpoint, IDataBreakpointOptions, IInstructionBreakpointOptions, InstructionBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; +import { Breakpoint, DataBreakpoint, DebugModel, FunctionBreakpoint, IDataBreakpointOptions, IFunctionBreakpointOptions, IInstructionBreakpointOptions, InstructionBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; import { DebugStorage } from 'vs/workbench/contrib/debug/common/debugStorage'; import { DebugTelemetry } from 'vs/workbench/contrib/debug/common/debugTelemetry'; @@ -1073,8 +1073,14 @@ export class DebugService implements IDebugService { return this.sendAllBreakpoints(); } - addFunctionBreakpoint(name?: string, id?: string, mode?: string): void { - this.model.addFunctionBreakpoint(name || '', id, mode); + async addFunctionBreakpoint(opts?: IFunctionBreakpointOptions, id?: string): Promise { + this.model.addFunctionBreakpoint(opts ?? { name: '' }, id); + // If opts not provided, sending the breakpoint is handled by a later to call to `updateFunctionBreakpoint` + if (opts) { + this.debugStorage.storeBreakpoints(this.model); + await this.sendFunctionBreakpoints(); + this.debugStorage.storeBreakpoints(this.model); + } } async updateFunctionBreakpoint(id: string, update: { name?: string; hitCondition?: string; condition?: string }): Promise { diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 4b8445dc43d..e48607b9eda 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -24,7 +24,7 @@ import { ITelemetryEndpoint } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IEditorPane } from 'vs/workbench/common/editor'; import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; -import { IDataBreakpointOptions, IInstructionBreakpointOptions } from 'vs/workbench/contrib/debug/common/debugModel'; +import { IDataBreakpointOptions, IFunctionBreakpointOptions, IInstructionBreakpointOptions } from 'vs/workbench/contrib/debug/common/debugModel'; import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; import { ITaskIdentifier } from 'vs/workbench/contrib/tasks/common/tasks'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -1150,7 +1150,7 @@ export interface IDebugService { /** * Adds a new function breakpoint for the given name. */ - addFunctionBreakpoint(name?: string, id?: string, mode?: string): void; + addFunctionBreakpoint(opts?: IFunctionBreakpointOptions, id?: string): void; /** * Updates an already existing function breakpoint. diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index b12e9b726ff..707d5aba0b4 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -1896,8 +1896,8 @@ export class DebugModel extends Disposable implements IDebugModel { this._onDidChangeBreakpoints.fire({ changed: changed, sessionOnly: false }); } - addFunctionBreakpoint(functionName: string, id?: string, mode?: string): IFunctionBreakpoint { - const newFunctionBreakpoint = new FunctionBreakpoint({ name: functionName, mode }, id); + addFunctionBreakpoint(opts: IFunctionBreakpointOptions, id?: string): IFunctionBreakpoint { + const newFunctionBreakpoint = new FunctionBreakpoint(opts, id); this.functionBreakpoints.push(newFunctionBreakpoint); this._onDidChangeBreakpoints.fire({ added: [newFunctionBreakpoint], sessionOnly: false }); diff --git a/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts index 61599c36ce9..6b47aa1dc3f 100644 --- a/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts @@ -161,8 +161,8 @@ suite('Debug - Breakpoints', () => { }); test('function breakpoints', () => { - model.addFunctionBreakpoint('foo', '1'); - model.addFunctionBreakpoint('bar', '2'); + model.addFunctionBreakpoint({ name: 'foo' }, '1'); + model.addFunctionBreakpoint({ name: 'bar' }, '2'); model.updateFunctionBreakpoint('1', { name: 'fooUpdated' }); model.updateFunctionBreakpoint('2', { name: 'barUpdated' }); @@ -380,7 +380,7 @@ suite('Debug - Breakpoints', () => { assert.strictEqual(result.message, 'Data Breakpoint'); assert.strictEqual(result.icon.id, 'debug-breakpoint-data'); - const functionBreakpoint = model.addFunctionBreakpoint('foo', '1'); + const functionBreakpoint = model.addFunctionBreakpoint({ name: 'foo' }, '1'); result = getBreakpointMessageAndIcon(State.Stopped, true, functionBreakpoint, ls, model); assert.strictEqual(result.message, 'Function Breakpoint'); assert.strictEqual(result.icon.id, 'debug-breakpoint-function'); From 8d653939de9513108c2e5fd51cd6c5d285557912 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 8 May 2024 20:39:58 +0200 Subject: [PATCH 056/357] voice - use `mute` and `unmute` for chat responses to trigger synthesis (#212283) --- .../actions/voiceChatActions.ts | 75 +++++++++++++++---- .../electron-sandbox/chat.contribution.ts | 3 +- 2 files changed, 61 insertions(+), 17 deletions(-) diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts index 36cc1487c93..fcd736bca72 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts @@ -38,9 +38,9 @@ import { CHAT_CATEGORY, stringifyItem } from 'vs/workbench/contrib/chat/browser/ import { IChatExecuteActionContext } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions'; import { CHAT_VIEW_ID, IChatWidget, IChatWidgetService, IQuickChatService, showChatView } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatAgentLocation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { CONTEXT_CHAT_REQUEST_IN_PROGRESS, CONTEXT_IN_CHAT_INPUT, CONTEXT_CHAT_ENABLED, CONTEXT_RESPONSE_FILTERED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { CONTEXT_CHAT_REQUEST_IN_PROGRESS, CONTEXT_IN_CHAT_INPUT, CONTEXT_CHAT_ENABLED, CONTEXT_RESPONSE, CONTEXT_RESPONSE_FILTERED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatService, KEYWORD_ACTIVIATION_SETTING_ID } from 'vs/workbench/contrib/chat/common/chatService'; -import { isRequestVM, isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; +import { isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { IVoiceChatService } from 'vs/workbench/contrib/chat/common/voiceChat'; import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; import { InlineChatController } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; @@ -864,18 +864,24 @@ export class ReadChatItemAloud extends Action2 { title: localize2('workbench.action.chat.readChatItemAloud', "Read Aloud"), f1: false, category: CHAT_CATEGORY, + icon: Codicon.unmute, precondition: CanVoiceChat, menu: { - id: MenuId.ChatContext, - when: ContextKeyExpr.and(CanVoiceChat, CONTEXT_RESPONSE_FILTERED.negate()), - group: 'textToSpeech' + id: MenuId.ChatMessageTitle, + when: ContextKeyExpr.and( + CanVoiceChat, + CONTEXT_RESPONSE, // only for responses + TextToSpeechInProgress.negate(), // but not when already in progress + CONTEXT_RESPONSE_FILTERED.negate() // and not when response is filtered + ), + group: 'navigation' } }); } run(accessor: ServicesAccessor, ...args: any[]) { const item = args[0]; - if (!isRequestVM(item) && !isResponseVM(item)) { + if (!isResponseVM(item)) { return; } @@ -899,16 +905,53 @@ export class StopReadAloud extends Action2 { weight: KeybindingWeight.WorkbenchContrib + 100, primary: KeyCode.Escape, }, - menu: [{ - id: MenuId.ChatContext, - when: TextToSpeechInProgress, - group: 'textToSpeech' - }, { - id: MenuId.ChatExecute, - when: TextToSpeechInProgress, - group: 'navigation', - order: -1 - }] + menu: [ + { + id: MenuId.ChatExecute, + when: TextToSpeechInProgress, + group: 'navigation', + order: -1 + }, + { + id: MenuId.for('terminalChatInput'), + when: TextToSpeechInProgress, + group: 'navigation', + order: -1 + } + ] + }); + } + + async run(accessor: ServicesAccessor, ...args: any[]) { + ChatSynthesizerSessions.getInstance(accessor.get(IInstantiationService)).stop(); + } +} + +export class StopReadChatItemAloud extends Action2 { + + static readonly ID = 'workbench.action.chat.stopRadChatItemAloud'; + + constructor() { + super({ + id: StopReadChatItemAloud.ID, + icon: Codicon.mute, + title: localize2('workbench.action.chat.stopRadChatItemAloud', "Stop Reading Aloud"), + precondition: TextToSpeechInProgress, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib + 100, + primary: KeyCode.Escape, + }, + menu: [ + { + id: MenuId.ChatMessageTitle, + when: ContextKeyExpr.and( + TextToSpeechInProgress, // only when in progress + CONTEXT_RESPONSE, // only for responses + CONTEXT_RESPONSE_FILTERED.negate() // but not when response is filtered + ), + group: 'navigation' + } + ] }); } diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts b/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts index 5ef9c6a6c67..45a4d8be6c7 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { InlineVoiceChatAction, QuickVoiceChatAction, StartVoiceChatAction, StopListeningInQuickChatAction, StopListeningInChatEditorAction, StopListeningInChatViewAction, VoiceChatInChatViewAction, StopListeningAction, StopListeningAndSubmitAction, KeywordActivationContribution, InstallVoiceChatAction, StopListeningInTerminalChatAction, HoldToVoiceChatInChatViewAction, ReadChatItemAloud, StopReadAloud } from 'vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions'; +import { InlineVoiceChatAction, QuickVoiceChatAction, StartVoiceChatAction, StopListeningInQuickChatAction, StopListeningInChatEditorAction, StopListeningInChatViewAction, VoiceChatInChatViewAction, StopListeningAction, StopListeningAndSubmitAction, KeywordActivationContribution, InstallVoiceChatAction, StopListeningInTerminalChatAction, HoldToVoiceChatInChatViewAction, ReadChatItemAloud, StopReadAloud, StopReadChatItemAloud } from 'vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions'; import { registerAction2 } from 'vs/platform/actions/common/actions'; import { WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; @@ -24,6 +24,7 @@ registerAction2(StopListeningInQuickChatAction); registerAction2(StopListeningInTerminalChatAction); registerAction2(ReadChatItemAloud); +registerAction2(StopReadChatItemAloud); registerAction2(StopReadAloud); registerWorkbenchContribution2(KeywordActivationContribution.ID, KeywordActivationContribution, WorkbenchPhase.AfterRestored); From 9c336bfa87fbc9f760439912a323ac77fbaf9484 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 8 May 2024 11:41:49 -0700 Subject: [PATCH 057/357] Add test for FunctionBreakpoint change (#212284) See #211894 --- .../src/singlefolder-tests/debug.test.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts index ee33d61d1e0..cc2f2675297 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { basename } from 'path'; -import { commands, debug, Disposable, window, workspace } from 'vscode'; +import { commands, debug, Disposable, FunctionBreakpoint, window, workspace } from 'vscode'; import { assertNoRpc, createRandomFile, disposeAll } from '../utils'; suite('vscode API - debug', function () { @@ -49,6 +49,17 @@ suite('vscode API - debug', function () { disposeAll(toDispose); }); + test('function breakpoint', async function () { + assert.strictEqual(debug.breakpoints.length, 0); + debug.addBreakpoints([new FunctionBreakpoint('func', false, 'condition', 'hitCondition', 'logMessage')]); + const functionBreakpoint = debug.breakpoints[0] as FunctionBreakpoint; + assert.strictEqual(functionBreakpoint.condition, 'condition'); + assert.strictEqual(functionBreakpoint.hitCondition, 'hitCondition'); + assert.strictEqual(functionBreakpoint.logMessage, 'logMessage'); + assert.strictEqual(functionBreakpoint.enabled, false); + assert.strictEqual(functionBreakpoint.functionName, 'func'); + }); + test('start debugging', async function () { let stoppedEvents = 0; let variablesReceived: () => void; From bd2df940d74b51105aefb11304e028d2fb56a9dc Mon Sep 17 00:00:00 2001 From: Aaron Munger Date: Wed, 8 May 2024 12:40:08 -0700 Subject: [PATCH 058/357] save to EH synchronously (#212193) * only save on EH if we can do so synchronously * clean up * update test * test cached provider * revert to defining save method property, set property as soon as serializer is available --- .../workbench/api/common/extHostNotebook.ts | 6 ++- .../notebook/common/notebookEditorModel.ts | 23 +++++---- .../test/browser/notebookEditorModel.test.ts | 47 ++++++++++++++++++- 3 files changed, 62 insertions(+), 14 deletions(-) diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index 75dc5a402ff..29e72e73a07 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -331,14 +331,13 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { throw new files.FileOperationError(localize('err.readonly', "Unable to modify read-only file '{0}'", this._resourceForError(uri)), files.FileOperationResult.FILE_PERMISSION_DENIED); } - // validate write - await this._validateWriteFile(uri, options); const data: vscode.NotebookData = { metadata: filter(document.apiNotebook.metadata, key => !(serializer.options?.transientDocumentMetadata ?? {})[key]), cells: [], }; + // this data must be retrieved before any async calls to ensure the data is for the correct version for (const cell of document.apiNotebook.getCells()) { const cellData = new extHostTypes.NotebookCellData( cell.kind, @@ -354,6 +353,9 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { data.cells.push(cellData); } + // validate write + await this._validateWriteFile(uri, options); + const bytes = await serializer.serializer.serializeNotebook(data, token); await this._extHostFileSystem.value.writeFile(uri, bytes); const providerExtUri = this._extHostFileSystem.getFileSystemProviderExtUri(uri.scheme); diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index be5a5ee4ed2..d96ad9d32a5 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -230,19 +230,22 @@ export class NotebookFileWorkingCopyModel extends Disposable implements IStoredF // Override save behavior to avoid transferring the buffer across the wire 3 times if (saveWithReducedCommunication) { - this.save = async (options: IWriteFileOptions, token: CancellationToken) => { - const serializer = await this.getNotebookSerializer(); - - if (token.isCancellationRequested) { - throw new CancellationError(); - } - - const stat = await serializer.save(this._notebookModel.uri, this._notebookModel.versionId, options, token); - return stat; - }; + this.setSaveDelegate().catch(console.error); } } + private async setSaveDelegate() { + const serializer = await this.getNotebookSerializer(); + this.save = async (options: IWriteFileOptions, token: CancellationToken) => { + if (token.isCancellationRequested) { + throw new CancellationError(); + } + + const stat = await serializer.save(this._notebookModel.uri, this._notebookModel.versionId, options, token); + return stat; + }; + } + override dispose(): void { this._notebookModel.dispose(); super.dispose(); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts index 16f6b0728a3..8d1d368dbd7 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts @@ -13,6 +13,7 @@ import { mock } from 'vs/base/test/common/mock'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { IFileStatWithMetadata } from 'vs/platform/files/common/files'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { CellKind, IOutputDto, NotebookData, NotebookSetting, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -242,14 +243,56 @@ suite('NotebookFileWorkingCopyModel', function () { assert.strictEqual(callCount, 1); }); + + test('Notebook model will not return a save delegate if the serializer has not been retreived', async function () { + const notebook = instantiationService.createInstance(NotebookTextModel, + 'notebook', + URI.file('test'), + [{ cellKind: CellKind.Code, language: 'foo', mime: 'foo', source: 'foo', outputs: [], metadata: { foo: 123, bar: 456 } }], + {}, + { transientCellMetadata: {}, transientDocumentMetadata: {}, cellContentMetadata: {}, transientOutputs: false, } + ); + disposables.add(notebook); + + const serializer = new class extends mock() { + override save(): Promise { + return Promise.resolve({ name: 'savedFile' } as IFileStatWithMetadata); + } + }; + (serializer as any).test = 'yes'; + + let resolveSerializer: (serializer: INotebookSerializer) => void = () => { }; + const serializerPromise = new Promise(resolve => { + resolveSerializer = resolve; + }); + const notebookService = mockNotebookService(notebook, serializerPromise); + configurationService.setUserConfiguration(NotebookSetting.remoteSaving, true); + + const model = disposables.add(new NotebookFileWorkingCopyModel( + notebook, + notebookService, + configurationService + )); + + // the save method should not be set if the serializer is not yet resolved + const notExist = model.save; + assert.strictEqual(notExist, undefined); + + resolveSerializer(serializer); + await model.getNotebookSerializer(); + const result = await model.save?.({} as any, {} as any); + + assert.strictEqual(result!.name, 'savedFile'); + }); }); -function mockNotebookService(notebook: NotebookTextModel, notebookSerializer: INotebookSerializer) { +function mockNotebookService(notebook: NotebookTextModel, notebookSerializer: Promise | INotebookSerializer) { return new class extends mock() { override async withNotebookDataProvider(viewType: string): Promise { + const serializer = await notebookSerializer; return new SimpleNotebookProviderInfo( notebook.viewType, - notebookSerializer, + serializer, { id: new ExtensionIdentifier('test'), location: undefined From afb251a69400e8fa65b47d4aca833b620ad3cc1d Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 8 May 2024 15:01:23 -0700 Subject: [PATCH 059/357] wip --- .../package.json | 7 +- .../src/coverageProvider.ts | 117 ++++++++++++- .../src/extension.ts | 29 ++-- .../src/testOutputScanner.ts | 18 +- .../src/v8CoverageWrangling.test.ts | 106 ++++++++++++ .../src/v8CoverageWrangling.ts | 156 ++++++++++++++++++ .../tsconfig.json | 3 +- .../vscode-selfhost-test-provider/yarn.lock | 23 ++- 8 files changed, 437 insertions(+), 22 deletions(-) create mode 100644 .vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.test.ts create mode 100644 .vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.ts diff --git a/.vscode/extensions/vscode-selfhost-test-provider/package.json b/.vscode/extensions/vscode-selfhost-test-provider/package.json index f27953f3cdb..cda2d07ddf3 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/package.json +++ b/.vscode/extensions/vscode-selfhost-test-provider/package.json @@ -65,10 +65,13 @@ "license": "MIT", "scripts": { "compile": "gulp compile-extension:vscode-selfhost-test-provider", - "watch": "gulp watch-extension:vscode-selfhost-test-provider" + "watch": "gulp watch-extension:vscode-selfhost-test-provider", + "test": "npx mocha --ui tdd 'out/*.test.js'" }, "devDependencies": { - "@types/node": "18.x" + "@types/mocha": "^10.0.6", + "@types/node": "18.x", + "v8-to-istanbul": "^9.2.0" }, "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/coverageProvider.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/coverageProvider.ts index 16fa7843336..dd100be9a26 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/coverageProvider.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/coverageProvider.ts @@ -4,5 +4,120 @@ *--------------------------------------------------------------------------------------------*/ import { IstanbulCoverageContext } from 'istanbul-to-vscode'; +import { SourceMapStore } from './testOutputScanner'; +import * as vscode from 'vscode'; +import { IScriptCoverage, OffsetToPosition, RangeCoverageTracker } from './v8CoverageWrangling'; +import * as v8ToIstanbul from 'v8-to-istanbul'; -export const coverageContext = new IstanbulCoverageContext(); +export const istanbulCoverageContext = new IstanbulCoverageContext(); + +/** + * Tracks coverage in per-script coverage mode. There are two modes of coverage + * in this extension: generic istanbul reports, and reports from the runtime + * sent before and after each test case executes. This handles the latter. + */ +export class PerTestCoverageTracker { + private readonly scripts = new Map(); + + constructor( + private readonly initialCoverage: IScriptCoverage, + private readonly maps: SourceMapStore, + ) {} + + public add(coverage: IScriptCoverage, test?: vscode.TestItem) { + const script = this.scripts.get(coverage.scriptId); + if (script) { + return script.add(coverage, test); + } + if (!coverage.source) { + throw new Error('expected to have source the first time a script is seen'); + } + + const src = new Script(coverage.url, coverage.source, this.maps); + } +} + +class Script { + private converter: OffsetToPosition; + + /** Tracking the overall coverage for the file */ + private overall = new ScriptProjection(); + /** Range tracking per-test item */ + private readonly perItem = new Map(); + + constructor( + public readonly url: string, + source: string, + private readonly maps: SourceMapStore, + ) { + this.converter = new OffsetToPosition(source); + } + + public add(coverage: IScriptCoverage, test?: vscode.TestItem) { + this.overall.add(coverage); + if (test) { + const p = new ScriptProjection(); + p.add(coverage); + this.perItem.set(test, p); + } + } + + public report(run: vscode.TestRun) { + + } +} + +class ScriptProjection { + /** Range tracking for non-block coverage in the file */ + private file = new RangeCoverageTracker(); + /** Range tracking for block coverage in the file */ + private readonly blocks = new Map(); + + public add(coverage: IScriptCoverage) { + + for (const fn of coverage.functions) { + if (fn.isBlockCoverage) { + const key = `${fn.ranges[0].startOffset}/${fn.ranges[0].endOffset}`; + const block = this.blocks.get(key); + if (block) { + for (let i = 1; i < fn.ranges.length; i++) { + block.setCovered(fn.ranges[i].startOffset, fn.ranges[i].endOffset, fn.ranges[i].count > 0); + } + } else { + this.blocks.set(key, RangeCoverageTracker.initializeBlock(fn.ranges)); + } + } else { + for (const range of fn.ranges) { + this.file.setCovered(range.startOffset, range.endOffset, range.count > 0); + } + } + } + } + + public report(run: vscode.TestRun, convert: OffsetToPosition, item?: vscode.TestItem) { + const ranges = [...this.file]; + for (const block of this.blocks.values()) { + for (const range of block) { + ranges.push(range); + } + } + + let ri = 0; + ranges.sort((a, b) => a.end - b.end); + + let offset = 0; + for (let i = 0; i < convert.lines.length; i++) { + const lineEnd = offset + convert.lines[i] + 1; + + const coverage = new RangeCoverageTracker(); + for (let i = ri; i < ranges.length && ranges[i].start < lineEnd; i++) { + coverage.setCovered(ranges[i].start - offset, ranges[i].end - offset, ranges[i].covered); + } + + while (ri < ranges.length && ranges[ri].end < lineEnd) { + ri++; + } + } + + } +} diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts index 153f5efb2d9..cda561e1325 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts @@ -7,7 +7,7 @@ import { randomBytes } from 'crypto'; import { tmpdir } from 'os'; import * as path from 'path'; import * as vscode from 'vscode'; -import { coverageContext } from './coverageProvider'; +import { istanbulCoverageContext } from './coverageProvider'; import { FailingDeepStrictEqualAssertFixer } from './failingDeepStrictEqualAssertFixer'; import { registerSnapshotUpdate } from './snapshot'; import { scanTestOutput } from './testOutputScanner'; @@ -86,17 +86,20 @@ export async function activate(context: vscode.ExtensionContext) { let coverageDir: string | undefined; let currentArgs = args; if (kind === vscode.TestRunProfileKind.Coverage) { - coverageDir = path.join(tmpdir(), `vscode-test-coverage-${randomBytes(8).toString('hex')}`); - currentArgs = [ - ...currentArgs, - '--coverage', - '--coveragePath', - coverageDir, - '--coverageFormats', - 'json', - '--coverageFormats', - 'html', - ]; + // todo: browser runs currently don't support per-test coverage + if (args.includes('--browser')) { + coverageDir = path.join(tmpdir(), `vscode-test-coverage-${randomBytes(8).toString('hex')}`); + currentArgs = [ + ...currentArgs, + '--coverage', + '--coveragePath', + coverageDir, + '--coverageFormats', + 'json', + ]; + } else { + currentArgs = [...currentArgs, '--per-test-coverage']; + } } return await scanTestOutput( @@ -180,7 +183,7 @@ export async function activate(context: vscode.ExtensionContext) { true ); - coverage.loadDetailedCoverage = coverageContext.loadDetailedCoverage; + coverage.loadDetailedCoverage = istanbulCoverageContext.loadDetailedCoverage; for (const [name, arg] of browserArgs) { const cfg = ctrl.createRunProfile( diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts index 39da7213325..6ed9b7fd973 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts @@ -12,7 +12,7 @@ import { import * as styles from 'ansi-styles'; import { ChildProcessWithoutNullStreams } from 'child_process'; import * as vscode from 'vscode'; -import { coverageContext } from './coverageProvider'; +import { IScriptCoverage, istanbulCoverageContext } from './coverageProvider'; import { attachTestMessageMetadata } from './metadata'; import { snapshotComment } from './snapshot'; import { getContentFromFilesystem } from './testTree'; @@ -24,6 +24,10 @@ export const enum MochaEvent { Pass = 'pass', Fail = 'fail', End = 'end', + + // custom events: + CoverageInit = 'coverage init', + CoverageIncrement = 'coverage increment', } export interface IStartEvent { @@ -62,12 +66,20 @@ export interface IEndEvent { end: string /* ISO date */; } +export interface ITestCoverageCoverage { + file: string; + fullTitle: string; + coverage: IScriptCoverage; +} + export type MochaEventTuple = | [MochaEvent.Start, IStartEvent] | [MochaEvent.TestStart, ITestStartEvent] | [MochaEvent.Pass, IPassEvent] | [MochaEvent.Fail, IFailEvent] - | [MochaEvent.End, IEndEvent]; + | [MochaEvent.End, IEndEvent] + | [MochaEvent.CoverageInit, IScriptCoverage] + | [MochaEvent.CoverageIncrement, ITestCoverageCoverage]; const LF = '\n'.charCodeAt(0); @@ -315,7 +327,7 @@ export async function scanTestOutput( if (coverageDir) { try { - await coverageContext.apply(task, coverageDir, { + await istanbulCoverageContext.apply(task, coverageDir, { mapFileUri: uri => store.getSourceFile(uri.toString()), mapLocation: (uri, position) => store.getSourceLocation(uri.toString(), position.line, position.character), diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.test.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.test.ts new file mode 100644 index 00000000000..ad22e317860 --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.test.ts @@ -0,0 +1,106 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { RangeCoverageTracker } from './v8CoverageWrangling'; + +suite('v8CoverageWrangling', () => { + suite('RangeCoverageTracker', () => { + test('covers new range', () => { + const rt = new RangeCoverageTracker(); + rt.cover(5, 10); + assert.deepStrictEqual([...rt], [{ start: 5, end: 10, covered: true }]); + }); + + test('non overlapping ranges', () => { + const rt = new RangeCoverageTracker(); + rt.cover(5, 10); + rt.cover(15, 20); + assert.deepStrictEqual([...rt], [ + { start: 5, end: 10, covered: true }, + { start: 15, end: 20, covered: true }, + ]); + }); + + test('covers exact', () => { + const rt = new RangeCoverageTracker(); + rt.uncovered(5, 10); + rt.cover(5, 10); + assert.deepStrictEqual([...rt], [ + { start: 5, end: 10, covered: true }, + ]); + }); + + test('overlap at start', () => { + const rt = new RangeCoverageTracker(); + rt.uncovered(5, 10); + rt.cover(2, 7); + assert.deepStrictEqual([...rt], [ + { start: 2, end: 5, covered: true }, + { start: 5, end: 7, covered: true }, + { start: 7, end: 10, covered: false }, + ]); + }); + + test('overlap at end', () => { + const rt = new RangeCoverageTracker(); + rt.cover(5, 10); + rt.uncovered(2, 7); + assert.deepStrictEqual([...rt], [ + { start: 2, end: 5, covered: false }, + { start: 5, end: 7, covered: true }, + { start: 7, end: 10, covered: true }, + ]); + }); + + test('inner contained', () => { + const rt = new RangeCoverageTracker(); + rt.cover(5, 10); + rt.uncovered(2, 12); + assert.deepStrictEqual([...rt], [ + { start: 2, end: 5, covered: false }, + { start: 5, end: 10, covered: true }, + { start: 10, end: 12, covered: false }, + ]); + }); + + test('outer contained', () => { + const rt = new RangeCoverageTracker(); + rt.uncovered(5, 10); + rt.cover(7, 9); + assert.deepStrictEqual([...rt], [ + { start: 5, end: 7, covered: false }, + { start: 7, end: 9, covered: true }, + { start: 9, end: 10, covered: false }, + ]); + }); + + test('boundary touching', () => { + const rt = new RangeCoverageTracker(); + rt.uncovered(5, 10); + rt.cover(10, 15); + rt.uncovered(15, 20); + assert.deepStrictEqual([...rt], [ + { start: 5, end: 10, covered: false }, + { start: 10, end: 15, covered: true }, + { start: 15, end: 20, covered: false }, + ]); + }); + + test('initializeBlock', () => { + const rt = RangeCoverageTracker.initializeBlock([ + { count: 1, startOffset: 5, endOffset: 30 }, + { count: 1, startOffset: 8, endOffset: 10 }, + { count: 0, startOffset: 15, endOffset: 20 }, + ]); + + assert.deepStrictEqual([...rt], [ + { start: 5, end: 15, covered: true }, + { start: 15, end: 20, covered: false }, + { start: 20, end: 30, covered: true }, + ]); + }); + }); +}); diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.ts new file mode 100644 index 00000000000..60ed7da4dd9 --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.ts @@ -0,0 +1,156 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export interface ICoverageRange { + start: number; + end: number; + covered: boolean; +} + +export interface IV8FunctionCoverage { + functionName: string; + isBlockCoverage: boolean; + ranges: IV8CoverageRange[]; +} + +export interface IV8CoverageRange { + startOffset: number; + endOffset: number; + count: number; +} + +/** V8 Script coverage data */ +export interface IScriptCoverage { + scriptId: string; + url: string; + // Script source added by the runner the first time the script is emitted. + source?: string; + functions: IV8FunctionCoverage[]; +} + + +export class RangeCoverageTracker implements Iterable { + /** + * A noncontiguous, non-overlapping, ordered set of ranges and whether + * that range has been covered. + */ + private ranges: readonly ICoverageRange[] = []; + + /** + * Adds a coverage tracker initialized for a function with {@link isBlockCoverage} set to true. + */ + public static initializeBlock(ranges: IV8CoverageRange[]) { + let start = ranges[0].startOffset; + const rt = new RangeCoverageTracker(); + if (!ranges[0].count) { + rt.uncovered(start, ranges[0].endOffset); + return rt; + } + + for (let i = 1; i < ranges.length; i++) { + const range = ranges[i]; + if (range.count) { + continue; + } + + rt.cover(start, range.startOffset); + rt.uncovered(range.startOffset, range.endOffset); + start = range.endOffset; + } + + rt.cover(start, ranges[0].endOffset); + return rt; + } + + /** Marks a range covered */ + public cover(start: number, end: number) { + this.setCovered(start, end, true); + } + + /** Marks a range as uncovered */ + public uncovered(start: number, end: number) { + this.setCovered(start, end, false); + } + + /** Iterates over coverage ranges */ + [Symbol.iterator]() { + return this.ranges[Symbol.iterator](); + } + + public setCovered(start: number, end: number, covered: boolean) { + const newRanges: ICoverageRange[] = []; + let i = 0; + for (; i < this.ranges.length && this.ranges[i].end <= start; i++) { + newRanges.push(this.ranges[i]); + } + + newRanges.push({ start, end, covered }); + for (; i < this.ranges.length; i++) { + const range = this.ranges[i]; + const last = newRanges[newRanges.length - 1]; + + if (range.start < last.start && range.end > last.end) { + // range contains last: + newRanges.pop(); + newRanges.push({ start: range.start, end: last.start, covered: range.covered }); + newRanges.push({ start: last.start, end: last.end, covered: range.covered || last.covered }); + newRanges.push({ start: last.end, end: range.end, covered: range.covered }); + } else if (range.start > last.start && range.end <= last.end) { + // last contains range: + newRanges.pop(); + newRanges.push({ start: last.start, end: range.start, covered: last.covered }); + newRanges.push({ start: range.start, end: range.end, covered: range.covered || last.covered }); + newRanges.push({ start: range.end, end: last.end, covered: last.covered }); + } else if (range.start < last.start && range.end <= last.end) { + // range overlaps start of last: + newRanges.pop(); + newRanges.push({ start: range.start, end: last.start, covered: range.covered }); + newRanges.push({ start: last.start, end: range.end, covered: range.covered || last.covered }); + newRanges.push({ start: range.end, end: last.end, covered: last.covered }); + } else if (range.start > last.start && range.end > last.end) { + // range overlaps end of last: + newRanges.pop(); + newRanges.push({ start: last.start, end: range.start, covered: last.covered }); + newRanges.push({ start: range.start, end: last.end, covered: range.covered || last.covered }); + newRanges.push({ start: last.end, end: range.end, covered: range.covered }); + } else { + // ranges are equal: + last.covered ||= range.covered; + } + } + + this.ranges = newRanges; + } +} + +export class OffsetToPosition { + /** Line numbers to byte offsets. */ + public readonly lines: number[] = []; + + constructor(public readonly source: string) { + this.lines.push(0); + for (let i = source.indexOf('\n'); i !== -1; i = source.indexOf('\n', i + 1)) { + this.lines.push(i + 1); + } + } + + /** + * Converts from a file offset to a base 0 line/column . + */ + public convert(offset: number): { line: number; column: number } { + let low = 0; + let high = this.lines.length; + while (low < high) { + const mid = Math.floor((low + high) / 2); + if (this.lines[mid] > offset) { + high = mid; + } else { + low = mid + 1; + } + } + + return { line: low - 1, column: offset - this.lines[low - 1] }; + } +} diff --git a/.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json b/.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json index 4bc025b62ba..b21867bb03d 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json +++ b/.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json @@ -3,7 +3,8 @@ "compilerOptions": { "outDir": "./out", "types": [ - "node" + "node", + "mocha", ] }, "include": [ diff --git a/.vscode/extensions/vscode-selfhost-test-provider/yarn.lock b/.vscode/extensions/vscode-selfhost-test-provider/yarn.lock index bf2295ed7b3..bff97849843 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/yarn.lock +++ b/.vscode/extensions/vscode-selfhost-test-provider/yarn.lock @@ -12,7 +12,7 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== -"@jridgewell/trace-mapping@^0.3.25": +"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.25": version "0.3.25" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== @@ -20,11 +20,16 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@types/istanbul-lib-coverage@^2.0.6": +"@types/istanbul-lib-coverage@^2.0.1", "@types/istanbul-lib-coverage@^2.0.6": version "2.0.6" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7" integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w== +"@types/mocha@^10.0.6": + version "10.0.6" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-10.0.6.tgz#818551d39113081048bdddbef96701b4e8bb9d1b" + integrity sha512-dJvrYWxP/UcXm36Qn36fxhUKu8A/xMRXVT2cliFF1Z7UA9liG5Psj3ezNSZw+5puH2czDXRLcXQxf8JbJt0ejg== + "@types/node@18.x": version "18.19.26" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.26.tgz#18991279d0a0e53675285e8cf4a0823766349729" @@ -37,6 +42,11 @@ ansi-styles@^5.2.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + istanbul-to-vscode@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/istanbul-to-vscode/-/istanbul-to-vscode-2.0.1.tgz#84994d06e604b68cac7301840f338b1e74eb888b" @@ -48,3 +58,12 @@ undici-types@~5.26.4: version "5.26.5" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + +v8-to-istanbul@^9.2.0: + version "9.2.0" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz#2ed7644a245cddd83d4e087b9b33b3e62dfd10ad" + integrity sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA== + dependencies: + "@jridgewell/trace-mapping" "^0.3.12" + "@types/istanbul-lib-coverage" "^2.0.1" + convert-source-map "^2.0.0" From 2aa1079dbb407a998f9aa9b56049c291a586496e Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 8 May 2024 15:42:43 -0700 Subject: [PATCH 060/357] just use v8-to-istanbul --- .../package.json | 7 +- .../src/coverageProvider.ts | 157 ++++++++---------- .../src/sourceMapStore.ts | 102 ++++++++++++ .../src/testOutputScanner.ts | 108 ++---------- .../src/v8CoverageWrangling.test.ts | 106 ------------ .../src/v8CoverageWrangling.ts | 156 ----------------- .../tsconfig.json | 2 +- .../vscode-selfhost-test-provider/yarn.lock | 5 - test/unit/electron/index.js | 40 ++++- test/unit/electron/renderer.js | 39 ++++- test/unit/fullJsonStreamReporter.js | 4 + 11 files changed, 264 insertions(+), 462 deletions(-) create mode 100644 .vscode/extensions/vscode-selfhost-test-provider/src/sourceMapStore.ts delete mode 100644 .vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.test.ts delete mode 100644 .vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.ts diff --git a/.vscode/extensions/vscode-selfhost-test-provider/package.json b/.vscode/extensions/vscode-selfhost-test-provider/package.json index cda2d07ddf3..80ecda4306f 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/package.json +++ b/.vscode/extensions/vscode-selfhost-test-provider/package.json @@ -69,13 +69,12 @@ "test": "npx mocha --ui tdd 'out/*.test.js'" }, "devDependencies": { - "@types/mocha": "^10.0.6", - "@types/node": "18.x", - "v8-to-istanbul": "^9.2.0" + "@types/node": "18.x" }, "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "ansi-styles": "^5.2.0", - "istanbul-to-vscode": "^2.0.1" + "istanbul-to-vscode": "^2.0.1", + "v8-to-istanbul": "^9.2.0" } } diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/coverageProvider.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/coverageProvider.ts index dd100be9a26..b27b69d3f53 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/coverageProvider.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/coverageProvider.ts @@ -3,121 +3,104 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { TraceMap } from '@jridgewell/trace-mapping'; import { IstanbulCoverageContext } from 'istanbul-to-vscode'; -import { SourceMapStore } from './testOutputScanner'; +import { fileURLToPath } from 'url'; import * as vscode from 'vscode'; -import { IScriptCoverage, OffsetToPosition, RangeCoverageTracker } from './v8CoverageWrangling'; -import * as v8ToIstanbul from 'v8-to-istanbul'; +import { SourceMapStore } from './sourceMapStore'; +import v8ToIstanbul = require('v8-to-istanbul'); export const istanbulCoverageContext = new IstanbulCoverageContext(); +export interface ICoverageRange { + start: number; + end: number; + covered: boolean; +} + +export interface IV8FunctionCoverage { + functionName: string; + isBlockCoverage: boolean; + ranges: IV8CoverageRange[]; +} + +export interface IV8CoverageRange { + startOffset: number; + endOffset: number; + count: number; +} + +/** V8 Script coverage data */ +export interface IScriptCoverage { + scriptId: string; + url: string; + // Script source added by the runner the first time the script is emitted. + source?: string; + functions: IV8FunctionCoverage[]; +} + /** * Tracks coverage in per-script coverage mode. There are two modes of coverage - * in this extension: generic istanbul reports, and reports from the runtime + * in this extension: generic istanbul reports, and V8 reports from the runtime * sent before and after each test case executes. This handles the latter. */ export class PerTestCoverageTracker { private readonly scripts = new Map(); - constructor( - private readonly initialCoverage: IScriptCoverage, - private readonly maps: SourceMapStore, - ) {} + constructor(private readonly maps: SourceMapStore) { } - public add(coverage: IScriptCoverage, test?: vscode.TestItem) { - const script = this.scripts.get(coverage.scriptId); - if (script) { - return script.add(coverage, test); - } - if (!coverage.source) { - throw new Error('expected to have source the first time a script is seen'); + /** Adds new coverage data to the run, optionally for a test item. */ + public add(run: vscode.TestRun, coverage: IScriptCoverage, test?: vscode.TestItem) { + let script = this.scripts.get(coverage.scriptId); + if (!script) { + if (!coverage.source) { + throw new Error('expected to have source the first time a script is seen'); + } + + script = new Script(coverage.url, coverage.source, this.maps); + this.scripts.set(coverage.scriptId, script); } - const src = new Script(coverage.url, coverage.source, this.maps); + return script.add(run, coverage, test); } } class Script { - private converter: OffsetToPosition; - - /** Tracking the overall coverage for the file */ - private overall = new ScriptProjection(); - /** Range tracking per-test item */ - private readonly perItem = new Map(); + private sourceMap?: Promise; + private originalContent?: Promise; constructor( public readonly url: string, - source: string, + private readonly source: string, private readonly maps: SourceMapStore, ) { - this.converter = new OffsetToPosition(source); } - public add(coverage: IScriptCoverage, test?: vscode.TestItem) { - this.overall.add(coverage); - if (test) { - const p = new ScriptProjection(); - p.add(coverage); - this.perItem.set(test, p); + public async add(run: vscode.TestRun, coverage: IScriptCoverage, test?: vscode.TestItem) { + if (!coverage.url.startsWith('file://')) { + return; } - } - public report(run: vscode.TestRun) { - - } -} - -class ScriptProjection { - /** Range tracking for non-block coverage in the file */ - private file = new RangeCoverageTracker(); - /** Range tracking for block coverage in the file */ - private readonly blocks = new Map(); - - public add(coverage: IScriptCoverage) { - - for (const fn of coverage.functions) { - if (fn.isBlockCoverage) { - const key = `${fn.ranges[0].startOffset}/${fn.ranges[0].endOffset}`; - const block = this.blocks.get(key); - if (block) { - for (let i = 1; i < fn.ranges.length; i++) { - block.setCovered(fn.ranges[i].startOffset, fn.ranges[i].endOffset, fn.ranges[i].count > 0); - } - } else { - this.blocks.set(key, RangeCoverageTracker.initializeBlock(fn.ranges)); - } - } else { - for (const range of fn.ranges) { - this.file.setCovered(range.startOffset, range.endOffset, range.count > 0); - } - } - } - } - - public report(run: vscode.TestRun, convert: OffsetToPosition, item?: vscode.TestItem) { - const ranges = [...this.file]; - for (const block of this.blocks.values()) { - for (const range of block) { - ranges.push(range); - } - } - - let ri = 0; - ranges.sort((a, b) => a.end - b.end); - - let offset = 0; - for (let i = 0; i < convert.lines.length; i++) { - const lineEnd = offset + convert.lines[i] + 1; - - const coverage = new RangeCoverageTracker(); - for (let i = ri; i < ranges.length && ranges[i].start < lineEnd; i++) { - coverage.setCovered(ranges[i].start - offset, ranges[i].end - offset, ranges[i].covered); - } - - while (ri < ranges.length && ranges[ri].end < lineEnd) { - ri++; - } - } + const sourceMap = await (this.sourceMap ??= this.maps.loadSourceMap(coverage.url)); + const originalSource = await (this.originalContent ??= this.maps.getSourceFileContents(coverage.url)); + const istanbuled = v8ToIstanbul(fileURLToPath(coverage.url), undefined, sourceMap && originalSource + ? { source: this.source, originalSource, sourceMap: { sourcemap: sourceMap } } + : { source: this.source } + ); + await istanbuled.load(); + + const coverages = await istanbulCoverageContext.fromJson(istanbuled.toIstanbul(), { + mapFileUri: uri => this.maps.getSourceFile(uri.toString()), + mapLocation: (uri, position) => + this.maps.getSourceLocation(uri.toString(), position.line, position.character), + }); + + for (const coverage of coverages) { + if (test) { + (coverage as vscode.FileCoverage2).testItem = test; + } + run.addCoverage(coverage); + } } } diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/sourceMapStore.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/sourceMapStore.ts new file mode 100644 index 00000000000..bc29b891fd2 --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/sourceMapStore.ts @@ -0,0 +1,102 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { + GREATEST_LOWER_BOUND, + LEAST_UPPER_BOUND, + originalPositionFor, + TraceMap +} from '@jridgewell/trace-mapping'; +import * as vscode from 'vscode'; +import { getContentFromFilesystem } from './testTree'; + +const inlineSourcemapRe = /^\/\/# sourceMappingURL=data:application\/json;base64,(.+)/m; +const sourceMapBiases = [GREATEST_LOWER_BOUND, LEAST_UPPER_BOUND] as const; + +export class SourceMapStore { + private readonly cache = new Map>(); + + async getSourceLocation(fileUri: string, line: number, col = 1) { + const sourceMap = await this.loadSourceMap(fileUri); + if (!sourceMap) { + return undefined; + } + + for (const bias of sourceMapBiases) { + const position = originalPositionFor(sourceMap, { column: col, line: line + 1, bias }); + if (position.line !== null && position.column !== null && position.source !== null) { + return new vscode.Location( + this.completeSourceMapUrl(sourceMap, position.source), + new vscode.Position(position.line - 1, position.column) + ); + } + } + + return undefined; + } + + async getSourceFile(compiledUri: string) { + const sourceMap = await this.loadSourceMap(compiledUri); + if (!sourceMap) { + return undefined; + } + + if (sourceMap.sources[0]) { + return this.completeSourceMapUrl(sourceMap, sourceMap.sources[0]); + } + + for (const bias of sourceMapBiases) { + const position = originalPositionFor(sourceMap, { column: 0, line: 1, bias }); + if (position.source !== null) { + return this.completeSourceMapUrl(sourceMap, position.source); + } + } + + return undefined; + } + + async getSourceFileContents(compiledUri: string) { + const sourceUri = await this.getSourceFile(compiledUri); + return sourceUri ? getContentFromFilesystem(sourceUri) : undefined; + } + + private completeSourceMapUrl(sm: TraceMap, source: string) { + if (sm.sourceRoot) { + try { + return vscode.Uri.parse(new URL(source, sm.sourceRoot).toString()); + } catch { + // ignored + } + } + + return vscode.Uri.parse(source); + } + + public loadSourceMap(fileUri: string) { + const existing = this.cache.get(fileUri); + if (existing) { + return existing; + } + + const promise = (async () => { + try { + const contents = await getContentFromFilesystem(vscode.Uri.parse(fileUri)); + const sourcemapMatch = inlineSourcemapRe.exec(contents); + if (!sourcemapMatch) { + return; + } + + const decoded = Buffer.from(sourcemapMatch[1], 'base64').toString(); + return new TraceMap(decoded, fileUri); + } catch (e) { + console.warn(`Error parsing sourcemap for ${fileUri}: ${(e as Error).stack}`); + return; + } + })(); + + this.cache.set(fileUri, promise); + return promise; + } +} diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts index 6ed9b7fd973..d0fec1fb606 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts @@ -3,19 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { - GREATEST_LOWER_BOUND, - LEAST_UPPER_BOUND, - originalPositionFor, - TraceMap, -} from '@jridgewell/trace-mapping'; import * as styles from 'ansi-styles'; import { ChildProcessWithoutNullStreams } from 'child_process'; import * as vscode from 'vscode'; -import { IScriptCoverage, istanbulCoverageContext } from './coverageProvider'; +import { IScriptCoverage, PerTestCoverageTracker, istanbulCoverageContext } from './coverageProvider'; import { attachTestMessageMetadata } from './metadata'; import { snapshotComment } from './snapshot'; -import { getContentFromFilesystem } from './testTree'; +import { SourceMapStore } from './sourceMapStore'; import { StreamSplitter } from './streamSplitter'; export const enum MochaEvent { @@ -174,6 +168,7 @@ export async function scanTestOutput( let lastTest: vscode.TestItem | undefined; let ranAnyTest = false; + let perTestCoverage: PerTestCoverageTracker | undefined; try { if (cancellation.isCancellationRequested) { @@ -319,6 +314,19 @@ export async function scanTestOutput( case MochaEvent.End: // no-op, we wait until the process exits to ensure coverage is written out break; + case MochaEvent.CoverageInit: + perTestCoverage ??= new PerTestCoverageTracker(store); + enqueueExitBlocker(perTestCoverage.add(task, evt[1])); + break; + case MochaEvent.CoverageIncrement: { + const { fullTitle, coverage } = evt[1]; + const tcase = tests.get(fullTitle); + if (tcase) { + perTestCoverage ??= new PerTestCoverageTracker(store); + enqueueExitBlocker(perTestCoverage.add(task, coverage, tcase)); + } + break; + } } }); }); @@ -400,90 +408,6 @@ const tryMakeMarkdown = (message: string) => { return new vscode.MarkdownString(lines.join('\n')); }; -const inlineSourcemapRe = /^\/\/# sourceMappingURL=data:application\/json;base64,(.+)/m; -const sourceMapBiases = [GREATEST_LOWER_BOUND, LEAST_UPPER_BOUND] as const; - -export class SourceMapStore { - private readonly cache = new Map>(); - - async getSourceLocation(fileUri: string, line: number, col = 1) { - const sourceMap = await this.loadSourceMap(fileUri); - if (!sourceMap) { - return undefined; - } - - for (const bias of sourceMapBiases) { - const position = originalPositionFor(sourceMap, { column: col, line: line + 1, bias }); - if (position.line !== null && position.column !== null && position.source !== null) { - return new vscode.Location( - this.completeSourceMapUrl(sourceMap, position.source), - new vscode.Position(position.line - 1, position.column) - ); - } - } - - return undefined; - } - - async getSourceFile(compiledUri: string) { - const sourceMap = await this.loadSourceMap(compiledUri); - if (!sourceMap) { - return undefined; - } - - if (sourceMap.sources[0]) { - return this.completeSourceMapUrl(sourceMap, sourceMap.sources[0]); - } - - for (const bias of sourceMapBiases) { - const position = originalPositionFor(sourceMap, { column: 0, line: 1, bias }); - if (position.source !== null) { - return this.completeSourceMapUrl(sourceMap, position.source); - } - } - - return undefined; - } - - private completeSourceMapUrl(sm: TraceMap, source: string) { - if (sm.sourceRoot) { - try { - return vscode.Uri.parse(new URL(source, sm.sourceRoot).toString()); - } catch { - // ignored - } - } - - return vscode.Uri.parse(source); - } - - private loadSourceMap(fileUri: string) { - const existing = this.cache.get(fileUri); - if (existing) { - return existing; - } - - const promise = (async () => { - try { - const contents = await getContentFromFilesystem(vscode.Uri.parse(fileUri)); - const sourcemapMatch = inlineSourcemapRe.exec(contents); - if (!sourcemapMatch) { - return; - } - - const decoded = Buffer.from(sourcemapMatch[1], 'base64').toString(); - return new TraceMap(decoded, fileUri); - } catch (e) { - console.warn(`Error parsing sourcemap for ${fileUri}: ${(e as Error).stack}`); - return; - } - })(); - - this.cache.set(fileUri, promise); - return promise; - } -} - const locationRe = /(file:\/{3}.+):([0-9]+):([0-9]+)/g; async function replaceAllLocations(store: SourceMapStore, str: string) { diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.test.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.test.ts deleted file mode 100644 index ad22e317860..00000000000 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.test.ts +++ /dev/null @@ -1,106 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as assert from 'assert'; -import { RangeCoverageTracker } from './v8CoverageWrangling'; - -suite('v8CoverageWrangling', () => { - suite('RangeCoverageTracker', () => { - test('covers new range', () => { - const rt = new RangeCoverageTracker(); - rt.cover(5, 10); - assert.deepStrictEqual([...rt], [{ start: 5, end: 10, covered: true }]); - }); - - test('non overlapping ranges', () => { - const rt = new RangeCoverageTracker(); - rt.cover(5, 10); - rt.cover(15, 20); - assert.deepStrictEqual([...rt], [ - { start: 5, end: 10, covered: true }, - { start: 15, end: 20, covered: true }, - ]); - }); - - test('covers exact', () => { - const rt = new RangeCoverageTracker(); - rt.uncovered(5, 10); - rt.cover(5, 10); - assert.deepStrictEqual([...rt], [ - { start: 5, end: 10, covered: true }, - ]); - }); - - test('overlap at start', () => { - const rt = new RangeCoverageTracker(); - rt.uncovered(5, 10); - rt.cover(2, 7); - assert.deepStrictEqual([...rt], [ - { start: 2, end: 5, covered: true }, - { start: 5, end: 7, covered: true }, - { start: 7, end: 10, covered: false }, - ]); - }); - - test('overlap at end', () => { - const rt = new RangeCoverageTracker(); - rt.cover(5, 10); - rt.uncovered(2, 7); - assert.deepStrictEqual([...rt], [ - { start: 2, end: 5, covered: false }, - { start: 5, end: 7, covered: true }, - { start: 7, end: 10, covered: true }, - ]); - }); - - test('inner contained', () => { - const rt = new RangeCoverageTracker(); - rt.cover(5, 10); - rt.uncovered(2, 12); - assert.deepStrictEqual([...rt], [ - { start: 2, end: 5, covered: false }, - { start: 5, end: 10, covered: true }, - { start: 10, end: 12, covered: false }, - ]); - }); - - test('outer contained', () => { - const rt = new RangeCoverageTracker(); - rt.uncovered(5, 10); - rt.cover(7, 9); - assert.deepStrictEqual([...rt], [ - { start: 5, end: 7, covered: false }, - { start: 7, end: 9, covered: true }, - { start: 9, end: 10, covered: false }, - ]); - }); - - test('boundary touching', () => { - const rt = new RangeCoverageTracker(); - rt.uncovered(5, 10); - rt.cover(10, 15); - rt.uncovered(15, 20); - assert.deepStrictEqual([...rt], [ - { start: 5, end: 10, covered: false }, - { start: 10, end: 15, covered: true }, - { start: 15, end: 20, covered: false }, - ]); - }); - - test('initializeBlock', () => { - const rt = RangeCoverageTracker.initializeBlock([ - { count: 1, startOffset: 5, endOffset: 30 }, - { count: 1, startOffset: 8, endOffset: 10 }, - { count: 0, startOffset: 15, endOffset: 20 }, - ]); - - assert.deepStrictEqual([...rt], [ - { start: 5, end: 15, covered: true }, - { start: 15, end: 20, covered: false }, - { start: 20, end: 30, covered: true }, - ]); - }); - }); -}); diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.ts deleted file mode 100644 index 60ed7da4dd9..00000000000 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.ts +++ /dev/null @@ -1,156 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -export interface ICoverageRange { - start: number; - end: number; - covered: boolean; -} - -export interface IV8FunctionCoverage { - functionName: string; - isBlockCoverage: boolean; - ranges: IV8CoverageRange[]; -} - -export interface IV8CoverageRange { - startOffset: number; - endOffset: number; - count: number; -} - -/** V8 Script coverage data */ -export interface IScriptCoverage { - scriptId: string; - url: string; - // Script source added by the runner the first time the script is emitted. - source?: string; - functions: IV8FunctionCoverage[]; -} - - -export class RangeCoverageTracker implements Iterable { - /** - * A noncontiguous, non-overlapping, ordered set of ranges and whether - * that range has been covered. - */ - private ranges: readonly ICoverageRange[] = []; - - /** - * Adds a coverage tracker initialized for a function with {@link isBlockCoverage} set to true. - */ - public static initializeBlock(ranges: IV8CoverageRange[]) { - let start = ranges[0].startOffset; - const rt = new RangeCoverageTracker(); - if (!ranges[0].count) { - rt.uncovered(start, ranges[0].endOffset); - return rt; - } - - for (let i = 1; i < ranges.length; i++) { - const range = ranges[i]; - if (range.count) { - continue; - } - - rt.cover(start, range.startOffset); - rt.uncovered(range.startOffset, range.endOffset); - start = range.endOffset; - } - - rt.cover(start, ranges[0].endOffset); - return rt; - } - - /** Marks a range covered */ - public cover(start: number, end: number) { - this.setCovered(start, end, true); - } - - /** Marks a range as uncovered */ - public uncovered(start: number, end: number) { - this.setCovered(start, end, false); - } - - /** Iterates over coverage ranges */ - [Symbol.iterator]() { - return this.ranges[Symbol.iterator](); - } - - public setCovered(start: number, end: number, covered: boolean) { - const newRanges: ICoverageRange[] = []; - let i = 0; - for (; i < this.ranges.length && this.ranges[i].end <= start; i++) { - newRanges.push(this.ranges[i]); - } - - newRanges.push({ start, end, covered }); - for (; i < this.ranges.length; i++) { - const range = this.ranges[i]; - const last = newRanges[newRanges.length - 1]; - - if (range.start < last.start && range.end > last.end) { - // range contains last: - newRanges.pop(); - newRanges.push({ start: range.start, end: last.start, covered: range.covered }); - newRanges.push({ start: last.start, end: last.end, covered: range.covered || last.covered }); - newRanges.push({ start: last.end, end: range.end, covered: range.covered }); - } else if (range.start > last.start && range.end <= last.end) { - // last contains range: - newRanges.pop(); - newRanges.push({ start: last.start, end: range.start, covered: last.covered }); - newRanges.push({ start: range.start, end: range.end, covered: range.covered || last.covered }); - newRanges.push({ start: range.end, end: last.end, covered: last.covered }); - } else if (range.start < last.start && range.end <= last.end) { - // range overlaps start of last: - newRanges.pop(); - newRanges.push({ start: range.start, end: last.start, covered: range.covered }); - newRanges.push({ start: last.start, end: range.end, covered: range.covered || last.covered }); - newRanges.push({ start: range.end, end: last.end, covered: last.covered }); - } else if (range.start > last.start && range.end > last.end) { - // range overlaps end of last: - newRanges.pop(); - newRanges.push({ start: last.start, end: range.start, covered: last.covered }); - newRanges.push({ start: range.start, end: last.end, covered: range.covered || last.covered }); - newRanges.push({ start: last.end, end: range.end, covered: range.covered }); - } else { - // ranges are equal: - last.covered ||= range.covered; - } - } - - this.ranges = newRanges; - } -} - -export class OffsetToPosition { - /** Line numbers to byte offsets. */ - public readonly lines: number[] = []; - - constructor(public readonly source: string) { - this.lines.push(0); - for (let i = source.indexOf('\n'); i !== -1; i = source.indexOf('\n', i + 1)) { - this.lines.push(i + 1); - } - } - - /** - * Converts from a file offset to a base 0 line/column . - */ - public convert(offset: number): { line: number; column: number } { - let low = 0; - let high = this.lines.length; - while (low < high) { - const mid = Math.floor((low + high) / 2); - if (this.lines[mid] > offset) { - high = mid; - } else { - low = mid + 1; - } - } - - return { line: low - 1, column: offset - this.lines[low - 1] }; - } -} diff --git a/.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json b/.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json index b21867bb03d..4ae0a106f09 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json +++ b/.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json @@ -4,12 +4,12 @@ "outDir": "./out", "types": [ "node", - "mocha", ] }, "include": [ "src/**/*", "../../../src/vscode-dts/vscode.d.ts", "../../../src/vscode-dts/vscode.proposed.testObserver.d.ts", + "../../../src/vscode-dts/vscode.proposed.attributableCoverage.d.ts", ] } diff --git a/.vscode/extensions/vscode-selfhost-test-provider/yarn.lock b/.vscode/extensions/vscode-selfhost-test-provider/yarn.lock index bff97849843..74a6f4caf5b 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/yarn.lock +++ b/.vscode/extensions/vscode-selfhost-test-provider/yarn.lock @@ -25,11 +25,6 @@ resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7" integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w== -"@types/mocha@^10.0.6": - version "10.0.6" - resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-10.0.6.tgz#818551d39113081048bdddbef96701b4e8bb9d1b" - integrity sha512-dJvrYWxP/UcXm36Qn36fxhUKu8A/xMRXVT2cliFF1Z7UA9liG5Psj3ezNSZw+5puH2czDXRLcXQxf8JbJt0ejg== - "@types/node@18.x": version "18.19.26" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.26.tgz#18991279d0a0e53675285e8cf4a0823766349729" diff --git a/test/unit/electron/index.js b/test/unit/electron/index.js index cfc2a5a9890..908fb2de97d 100644 --- a/test/unit/electron/index.js +++ b/test/unit/electron/index.js @@ -41,12 +41,13 @@ const minimist = require('minimist'); * coverage: boolean; * coveragePath: string; * coverageFormats: string | string[]; + * 'per-test-coverage': boolean; * help: boolean; * }} */ const args = minimist(process.argv.slice(2), { string: ['grep', 'run', 'runGlob', 'reporter', 'reporter-options', 'waitServer', 'timeout', 'crash-reporter-directory', 'tfs', 'coveragePath', 'coverageFormats'], - boolean: ['build', 'coverage', 'help', 'dev'], + boolean: ['build', 'coverage', 'help', 'dev', 'per-test-coverage'], alias: { 'grep': ['g', 'f'], 'runGlob': ['glob', 'runGrep'], @@ -68,10 +69,11 @@ Options: --runGlob, --glob, --runGrep only run tests matching --build run with build output (out-build) --coverage generate coverage report +--per-test-coverage generate a per-test V8 coverage report, only valid with the full-json-stream reporter --dev, --dev-tools, --devTools open dev tools, keep window open, reuse app data --reporter the mocha reporter (default: "spec") ---reporter-options the mocha reporter options (default: "") ---waitServer port to connect to and wait before running tests +--reporter-options the mocha reporter options (default: "") +--waitServer port to connect to and wait before running tests --timeout timeout for tests --crash-reporter-directory crash reporter directory --tfs TFS server URL @@ -160,7 +162,7 @@ function deserializeError(err) { class IPCRunner extends events.EventEmitter { - constructor() { + constructor(win) { super(); this.didFail = false; @@ -183,6 +185,34 @@ class IPCRunner extends events.EventEmitter { this.emit('fail', deserializeRunnable(test), deserializeError(err)); }); ipcMain.on('pending', (e, test) => this.emit('pending', deserializeRunnable(test))); + + ipcMain.handle('startCoverage', async () => { + win.webContents.debugger.attach(); + await win.webContents.debugger.sendCommand('Debugger.enable'); + await win.webContents.debugger.sendCommand('Profiler.enable'); + await win.webContents.debugger.sendCommand('Profiler.startPreciseCoverage', { + detailed: true, + allowTriggeredUpdates: false, + }); + }); + + const coverageScriptsReported = new Set(); + ipcMain.handle('snapshotCoverage', async (_, test) => { + const coverage = await win.webContents.debugger.sendCommand('Profiler.takePreciseCoverage'); + await Promise.all(coverage.result.map(async (r) => { + if (!coverageScriptsReported.has(r.scriptId)) { + coverageScriptsReported.add(r.scriptId); + const src = await win.webContents.debugger.sendCommand('Debugger.getScriptSource', { scriptId: r.scriptId }); + r.source = src.scriptSource; + } + })); + + if (!test) { + this.emit('coverage init', coverage); + } else { + this.emit('coverage increment', test, coverage); + } + }); } } @@ -274,7 +304,7 @@ app.on('ready', () => { win.loadURL(url.format({ pathname: path.join(__dirname, 'renderer.html'), protocol: 'file:', slashes: true })); - const runner = new IPCRunner(); + const runner = new IPCRunner(win); createStatsCollector(runner); // Handle renderer crashes, #117068 diff --git a/test/unit/electron/renderer.js b/test/unit/electron/renderer.js index fd22a6e9512..26e45f9ff9d 100644 --- a/test/unit/electron/renderer.js +++ b/test/unit/electron/renderer.js @@ -6,6 +6,7 @@ /*eslint-env mocha*/ const fs = require('fs'); +const inspector = require('inspector'); (function () { const originals = {}; @@ -169,9 +170,10 @@ function loadTestModules(opts) { }).then(loadModules); } -let currentTestTitle; +/** @type Mocha.Test */ +let currentTest; -function loadTests(opts) { +async function loadTests(opts) { //#region Unexpected Output @@ -185,6 +187,8 @@ function loadTests(opts) { _allowedTestOutput.push(/Deleting [0-9]+ old snapshots/); } + const perTestCoverage = opts['per-test-coverage'] ? await PerTestCoverage.init() : undefined; + const _allowedTestsWithOutput = new Set([ 'creates a snapshot', // self-testing 'validates a snapshot', // self-testing @@ -202,7 +206,7 @@ function loadTests(opts) { for (const consoleFn of [console.log, console.error, console.info, console.warn, console.trace, console.debug]) { console[consoleFn.name] = function (msg) { - if (!_allowedTestOutput.some(a => a.test(msg)) && !_allowedTestsWithOutput.has(currentTestTitle)) { + if (!_allowedTestOutput.some(a => a.test(msg)) && !_allowedTestsWithOutput.has(currentTest.title)) { _testsWithUnexpectedOutput = true; consoleFn.apply(console, arguments); } @@ -256,7 +260,7 @@ function loadTests(opts) { event.preventDefault(); // Do not log to test output, we show an error later when test ends event.stopPropagation(); - if (!_allowedTestsWithUnhandledRejections.has(currentTestTitle)) { + if (!_allowedTestsWithUnhandledRejections.has(currentTest.title)) { onUnexpectedError(event.reason); } }); @@ -275,7 +279,12 @@ function loadTests(opts) { }); }); - teardown(() => { + setup(async () => { + await perTestCoverage?.startTest(); + }); + + teardown(async () => { + await perTestCoverage?.finishTest(currentTest.file, currentTest.fullTitle()); // should not have unexpected output if (_testsWithUnexpectedOutput && !opts.dev) { @@ -410,7 +419,7 @@ function runTests(opts) { }); }); - runner.on('test', test => currentTestTitle = test.title); + runner.on('test', test => currentTest = test); if (opts.dev) { runner.on('fail', (test, err) => { @@ -432,3 +441,21 @@ ipcRenderer.on('run', (e, opts) => { ipcRenderer.send('error', err); }); }); + +class PerTestCoverage { + static async init() { + await ipcRenderer.invoke('startCoverage'); + return new PerTestCoverage(); + } + + async startTest() { + if (!this.didInit) { + this.didInit = true; + await ipcRenderer.invoke('snapshotCoverage'); + } + } + + async finishTest(file, fullTitle) { + await ipcRenderer.invoke('snapshotCoverage', { file, fullTitle }); + } +} diff --git a/test/unit/fullJsonStreamReporter.js b/test/unit/fullJsonStreamReporter.js index 07b2315a004..c92870cb0d8 100644 --- a/test/unit/fullJsonStreamReporter.js +++ b/test/unit/fullJsonStreamReporter.js @@ -29,6 +29,10 @@ module.exports = class FullJsonStreamReporter extends BaseRunner { runner.once(EVENT_RUN_BEGIN, () => writeEvent(['start', { total }])); runner.once(EVENT_RUN_END, () => writeEvent(['end', this.stats])); + // custom coverage events: + runner.on('coverage init', (c) => writeEvent(['coverageInit', c])); + runner.on('coverage increment', (context, c) => writeEvent(['coverageIncrement', context, c])); + runner.on(EVENT_TEST_BEGIN, test => writeEvent(['testStart', clean(test)])); runner.on(EVENT_TEST_PASS, test => writeEvent(['pass', clean(test)])); runner.on(EVENT_TEST_FAIL, (test, err) => { From 9cb0df2a0262442ee60a32a85b8d66c3e79a766d Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Wed, 8 May 2024 15:50:57 -0700 Subject: [PATCH 061/357] feat: support concurrent chat progress messages (#212300) * feat: support concurrent chat progress messages --- .../api/browser/mainThreadChatAgents2.ts | 24 +++++++++++++++- .../workbench/api/common/extHost.api.impl.ts | 1 + .../workbench/api/common/extHost.protocol.ts | 7 +++-- .../api/common/extHostChatAgents2.ts | 20 +++++++++---- .../api/common/extHostTypeConverters.ts | 20 ++++++++++++- src/vs/workbench/api/common/extHostTypes.ts | 9 ++++++ .../contrib/chat/browser/chatListRenderer.ts | 28 +++++++++++-------- .../contrib/chat/common/annotations.ts | 4 +-- .../contrib/chat/common/chatAgents.ts | 4 +-- .../contrib/chat/common/chatModel.ts | 20 +++++++++++-- .../contrib/chat/common/chatService.ts | 17 +++++++++++ .../contrib/chat/common/chatViewModel.ts | 4 +-- ...ode.proposed.chatParticipantAdditions.d.ts | 18 ++++++++++++ 13 files changed, 146 insertions(+), 30 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index 99bf1033707..69dd832a312 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { DeferredPromise } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Disposable, DisposableMap, IDisposable } from 'vs/base/common/lifecycle'; import { revive } from 'vs/base/common/marshalling'; @@ -44,6 +45,9 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA private readonly _pendingProgress = new Map void>(); private readonly _proxy: ExtHostChatAgentsShape2; + private _responsePartHandlePool = 0; + private readonly _activeResponsePartPromises = new Map>(); + constructor( extHostContext: IExtHostContext, @IChatAgentService private readonly _chatAgentService: IChatAgentService, @@ -166,7 +170,25 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA this._chatAgentService.updateAgent(data.id, revive(metadataUpdate)); } - async $handleProgressChunk(requestId: string, progress: IChatProgressDto): Promise { + async $handleProgressChunk(requestId: string, progress: IChatProgressDto, responsePartHandle?: number): Promise { + if (progress.kind === 'progressTask') { + const handle = ++this._responsePartHandlePool; + const responsePartId = `${requestId}_${handle}`; + const deferredContentPromise = new DeferredPromise(); + this._activeResponsePartPromises.set(responsePartId, deferredContentPromise); + this._pendingProgress.get(requestId)?.({ ...progress, task: () => deferredContentPromise.p, isSettled: () => deferredContentPromise.isSettled }); + return handle; + } else if (progress.kind === 'progressTaskResult' && responsePartHandle !== undefined) { + const responsePartId = `${requestId}_${responsePartHandle}`; + const deferredContentPromise = this._activeResponsePartPromises.get(responsePartId); + if (deferredContentPromise && progress.content) { + deferredContentPromise.complete(progress.content.value); + this._activeResponsePartPromises.delete(responsePartId); + } else { + deferredContentPromise?.complete(undefined); + } + return responsePartHandle; + } const revivedProgress = revive(progress); this._pendingProgress.get(requestId)?.(revivedProgress as IChatProgress); } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index fecf8932231..0a622e4d95a 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1704,6 +1704,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ChatResponseFileTreePart: extHostTypes.ChatResponseFileTreePart, ChatResponseAnchorPart: extHostTypes.ChatResponseAnchorPart, ChatResponseProgressPart: extHostTypes.ChatResponseProgressPart, + ChatResponseProgressPart2: extHostTypes.ChatResponseProgressPart2, ChatResponseReferencePart: extHostTypes.ChatResponseReferencePart, ChatResponseWarningPart: extHostTypes.ChatResponseWarningPart, ChatResponseTextEditPart: extHostTypes.ChatResponseTextEditPart, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 2dd52e38465..c0babcbd2be 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -52,7 +52,7 @@ import { IRevealOptions, ITreeItem, IViewBadge } from 'vs/workbench/common/views import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; import { ChatAgentLocation, IChatAgentMetadata, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatProgressResponseContent } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IChatFollowup, IChatProgress, IChatResponseErrorDetails, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatFollowup, IChatProgress, IChatResponseErrorDetails, IChatTask, IChatTaskDto, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatRequestVariableValue, IChatVariableData, IChatVariableResolverProgress } from 'vs/workbench/contrib/chat/common/chatVariables'; import { IChatMessage, IChatResponseFragment, ILanguageModelChatMetadata, ILanguageModelChatSelector, ILanguageModelsChangeEvent } from 'vs/workbench/contrib/chat/common/languageModels'; import { DebugConfigurationProviderTriggerKind, IAdapterDescriptor, IConfig, IDebugSessionReplMode, IDebugVisualization, IDebugVisualizationContext, IDebugVisualizationTreeItem, MainThreadDebugVisualization } from 'vs/workbench/contrib/debug/common/debug'; @@ -1238,7 +1238,7 @@ export interface MainThreadChatAgentsShape2 extends IDisposable { $unregisterAgentCompletionsProvider(handle: number): void; $updateAgent(handle: number, metadataUpdate: IExtensionChatAgentMetadata): void; $unregisterAgent(handle: number): void; - $handleProgressChunk(requestId: string, chunk: IChatProgressDto): Promise; + $handleProgressChunk(requestId: string, chunk: IChatProgressDto, handle?: number): Promise; $transferActiveChatSession(toWorkspace: UriComponents): void; } @@ -1253,7 +1253,8 @@ export interface IChatAgentCompletionItem { } export type IChatContentProgressDto = - | Dto; + | Dto> + | IChatTaskDto; export type IChatAgentHistoryEntryDto = { request: IChatAgentRequest; diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index d981bf150d2..885bb627bf0 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -68,12 +68,20 @@ class ChatAgentResponseStream { } } - const _report = (progress: Dto) => { + const _report = (progress: Dto, task?: () => Thenable) => { // Measure the time to the first progress update with real markdown content if (typeof this._firstProgress === 'undefined' && 'content' in progress) { this._firstProgress = this._stopWatch.elapsed(); } - this._proxy.$handleProgressChunk(this._request.requestId, progress); + + this._proxy.$handleProgressChunk(this._request.requestId, progress) + .then((handle) => { + if (typeof handle === 'number' && task) { + task().then((res) => { + this._proxy.$handleProgressChunk(this._request.requestId, typeConvert.ChatTaskResult.from(res), handle); + }); + } + }); }; this._apiObject = { @@ -116,11 +124,11 @@ class ChatAgentResponseStream { _report(dto); return this; }, - progress(value) { + progress(value, task?: (() => Thenable)) { throwIfDone(this.progress); - const part = new extHostTypes.ChatResponseProgressPart(value); - const dto = typeConvert.ChatResponseProgressPart.from(part); - _report(dto); + const part = new extHostTypes.ChatResponseProgressPart2(value, task); + const dto = task ? typeConvert.ChatTask.from(part) : typeConvert.ChatResponseProgressPart.from(part); + _report(dto, task); return this; }, warning(value) { diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index b0daab77b36..3021b228fb4 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -40,7 +40,7 @@ import { DEFAULT_EDITOR_ASSOCIATION, SaveReason } from 'vs/workbench/common/edit import { IViewBadge } from 'vs/workbench/common/views'; import { ChatAgentLocation, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatRequestVariableEntry } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IChatAgentDetection, IChatAgentMarkdownContentWithVulnerability, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgressMessage, IChatTextEdit, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatAgentDetection, IChatAgentMarkdownContentWithVulnerability, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgressMessage, IChatTask, IChatTaskResult, IChatTextEdit, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from 'vs/workbench/contrib/chat/common/chatService'; import * as chatProvider from 'vs/workbench/contrib/chat/common/languageModels'; import { DebugTreeItemCollapsibleState, IDebugVisualizationTreeItem } from 'vs/workbench/contrib/debug/common/debug'; import * as notebooks from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -2391,6 +2391,24 @@ export namespace ChatResponseWarningPart { } } +export namespace ChatTask { + export function from(part: vscode.ChatResponseProgressPart2): Dto { + return { + kind: 'progressTask', + content: MarkdownString.from(part.value), + }; + } +} + +export namespace ChatTaskResult { + export function from(part: string | void): Dto { + return { + kind: 'progressTaskResult', + content: typeof part === 'string' ? MarkdownString.from(part) : undefined + }; + } +} + export namespace ChatResponseCommandButtonPart { export function from(part: vscode.ChatResponseCommandButtonPart, commandsConverter: CommandsConverter, commandDisposables: DisposableStore): Dto { // If the command isn't in the converter, then this session may have been restored, and the command args don't exist anymore diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 63b6723fdf2..a21e39c192b 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -4374,6 +4374,15 @@ export class ChatResponseProgressPart { } } +export class ChatResponseProgressPart2 { + value: string; + task?: () => Thenable; + constructor(value: string, task?: () => Thenable) { + this.value = value; + this.task = task; + } +} + export class ChatResponseWarningPart { value: vscode.MarkdownString; constructor(value: string | vscode.MarkdownString) { diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index c7baf4327cb..ad2d8e5564b 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -70,7 +70,7 @@ import { ChatAgentLocation, IChatAgentMetadata, IChatAgentNameService } from 'vs import { CONTEXT_CHAT_RESPONSE_SUPPORT_ISSUE_REPORTING, CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_DETECTED_AGENT_COMMAND, CONTEXT_RESPONSE_FILTERED, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatProgressRenderableResponseContent, IChatTextEditGroup } from 'vs/workbench/contrib/chat/common/chatModel'; import { chatAgentLeader, chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { IChatCommandButton, IChatConfirmation, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseProgressFileTreeData, IChatSendRequestOptions, IChatService, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatCommandButton, IChatConfirmation, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseProgressFileTreeData, IChatSendRequestOptions, IChatService, IChatTask, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import { IChatProgressMessageRenderData, IChatRenderData, IChatResponseMarkdownRenderData, IChatResponseViewModel, IChatWelcomeMessageViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { IWordCountResult, getNWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; @@ -513,11 +513,12 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer): ReadonlyArray { - const result: Exclude[] = []; + const result: Exclude[] = []; for (const item of response) { const previousItem = result[result.length - 1]; if (item.kind === 'inlineReference') { diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index 277313e6d47..f303f0b56c6 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -25,13 +25,13 @@ import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storag import { CONTEXT_CHAT_ENABLED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatProgressResponseContent, IChatRequestVariableData } from 'vs/workbench/contrib/chat/common/chatModel'; import { IRawChatCommandContribution, RawChatParticipantLocation } from 'vs/workbench/contrib/chat/common/chatParticipantContribTypes'; -import { IChatFollowup, IChatProgress, IChatResponseErrorDetails } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatFollowup, IChatProgress, IChatResponseErrorDetails, IChatTaskDto } from 'vs/workbench/contrib/chat/common/chatService'; //#region agent service, commands etc export interface IChatAgentHistoryEntry { request: IChatAgentRequest; - response: ReadonlyArray; + response: ReadonlyArray; result: IChatAgentResult; } diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 3f456e6f2d7..dd896599889 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -20,7 +20,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ILogService } from 'vs/platform/log/common/log'; import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatRequestTextPart, IParsedChatRequest, getPromptText, reviveParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { IChatAgentMarkdownContentWithVulnerability, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgress, IChatProgressMessage, IChatResponseProgressFileTreeData, IChatTextEdit, IChatTreeData, IChatUsedContext, IChatWarningMessage, InteractiveSessionVoteDirection, isIUsedContext } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatAgentMarkdownContentWithVulnerability, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgress, IChatProgressMessage, IChatResponseProgressFileTreeData, IChatTask, IChatTextEdit, IChatTreeData, IChatUsedContext, IChatWarningMessage, InteractiveSessionVoteDirection, isIUsedContext } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables'; export interface IChatRequestVariableEntry { @@ -63,6 +63,7 @@ export type IChatProgressResponseContent = | IChatProgressMessage | IChatCommandButton | IChatWarningMessage + | IChatTask | IChatTextEditGroup | IChatConfirmation; @@ -170,7 +171,7 @@ export class Response implements IResponse { this._updateRepr(true); } - updateContent(progress: IChatProgressResponseContent | IChatTextEdit, quiet?: boolean): void { + updateContent(progress: IChatProgressResponseContent | IChatTextEdit | IChatTask, quiet?: boolean): void { if (progress.kind === 'markdownContent') { const responsePartLength = this._responseParts.length - 1; const lastResponsePart = this._responseParts[responsePartLength]; @@ -208,6 +209,20 @@ export class Response implements IResponse { } this._updateRepr(quiet); } + } else if (progress.kind === 'progressTask') { + // Add a new resolving part + const responsePosition = this._responseParts.push(progress) - 1; + this._updateRepr(quiet); + + if (progress.task) { + progress.task?.().then((content) => { + // Replace the resolving part's content with the resolved response + if (typeof content === 'string') { + this._responseParts[responsePosition] = { ...progress, content: new MarkdownString(content) }; + } + this._updateRepr(false); + }); + } } else { this._responseParts.push(progress); this._updateRepr(quiet); @@ -788,6 +803,7 @@ export class ChatModel extends Disposable implements IChatModel { progress.kind === 'command' || progress.kind === 'textEdit' || progress.kind === 'warning' || + progress.kind === 'progressTask' || progress.kind === 'confirmation' ) { request.response.updateContent(progress, quiet); diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index c9a9be9a42e..2664be3798c 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -106,6 +106,21 @@ export interface IChatProgressMessage { kind: 'progressMessage'; } +export interface IChatTask extends IChatTaskDto { + task?: () => Promise; + isSettled?: () => boolean; +} + +export interface IChatTaskDto { + content: IMarkdownString; + kind: 'progressTask'; +} + +export interface IChatTaskResult { + content: IMarkdownString | void; + kind: 'progressTaskResult'; +} + export interface IChatWarningMessage { content: IMarkdownString; kind: 'warning'; @@ -149,6 +164,8 @@ export type IChatProgress = | IChatContentInlineReference | IChatAgentDetection | IChatProgressMessage + | IChatTask + | IChatTaskResult | IChatCommandButton | IChatWarningMessage | IChatTextEdit diff --git a/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/chatViewModel.ts index 429d32cbc2c..37e0ea43d45 100644 --- a/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatViewModel.ts @@ -14,7 +14,7 @@ import { annotateVulnerabilitiesInText } from 'vs/workbench/contrib/chat/common/ import { IChatAgentCommand, IChatAgentData, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatModelInitState, IChatModel, IChatRequestModel, IChatResponseModel, IChatTextEditGroup, IChatWelcomeMessageContent, IResponse } from 'vs/workbench/contrib/chat/common/chatModel'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { IChatCommandButton, IChatConfirmation, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseErrorDetails, IChatResponseProgressFileTreeData, IChatUsedContext, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatCommandButton, IChatConfirmation, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseErrorDetails, IChatResponseProgressFileTreeData, IChatTask, IChatUsedContext, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; import { CodeBlockModelCollection } from './codeBlockModelCollection'; import { IMarkdownString } from 'vs/base/common/htmlContent'; @@ -95,7 +95,7 @@ export interface IChatProgressMessageRenderData { isLast: boolean; } -export type IChatRenderData = IChatResponseProgressFileTreeData | IChatResponseMarkdownRenderData | IChatProgressMessageRenderData | IChatCommandButton | IChatTextEditGroup | IChatConfirmation; +export type IChatRenderData = IChatResponseProgressFileTreeData | IChatResponseMarkdownRenderData | IChatProgressMessageRenderData | IChatCommandButton | IChatTextEditGroup | IChatConfirmation | IChatTask; export interface IChatResponseRenderData { renderedParts: IChatRenderData[]; } diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index 6ff2a8a71cf..d062407d47a 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -116,10 +116,28 @@ declare module 'vscode' { constructor(value: string | MarkdownString); } + export class ChatResponseProgressPart2 extends ChatResponseProgressPart { + value: string; + task?: () => Thenable; + constructor(value: string, task?: () => Thenable); + } + export interface ChatResponseStream { + + /** + * Push a progress part to this stream. Short-hand for + * `push(new ChatResponseProgressPart(value))`. + * + * @param value A progress message + * @param task If provided, a task to run while the progress is displayed. When the Thenable resolves, the progress will be marked complete in the UI, and the progress message will be updated to the resolved string if one is specified. + * @returns This stream. + */ + progress(value: string, task?: () => Thenable): ChatResponseStream; + textEdit(target: Uri, edits: TextEdit | TextEdit[]): ChatResponseStream; markdownWithVulnerabilities(value: string | MarkdownString, vulnerabilities: ChatVulnerability[]): ChatResponseStream; detectedParticipant(participant: string, command?: ChatCommand): ChatResponseStream; + push(part: ChatResponsePart | ChatResponseTextEditPart | ChatResponseDetectedParticipantPart | ChatResponseWarningPart | ChatResponseProgressPart2): ChatResponseStream; /** * Show an inline message in the chat view asking the user to confirm an action. From f921a48505d7a23b27750ba7d7d4dcc0b88720f2 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 8 May 2024 18:33:58 -0700 Subject: [PATCH 062/357] Move chat suggestions into their own file --- .../contrib/chat/browser/chat.contribution.ts | 1 + .../browser/contrib/chatInputCompletions.ts | 390 ++++++++++++++++++ .../browser/contrib/chatInputEditorContrib.ts | 386 +---------------- 3 files changed, 395 insertions(+), 382 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index de2ed8deb5f..9a6d586c99c 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -37,6 +37,7 @@ import { ChatWidgetService } from 'vs/workbench/contrib/chat/browser/chatWidget' import { ChatCodeBlockContextProviderService } from 'vs/workbench/contrib/chat/browser/codeBlockContextProviderService'; import 'vs/workbench/contrib/chat/browser/contrib/chatHistoryVariables'; import 'vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib'; +import 'vs/workbench/contrib/chat/browser/contrib/chatInputCompletions'; import { ChatAgentLocation, ChatAgentService, IChatAgentService, IChatAgentNameService, ChatAgentNameService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts new file mode 100644 index 00000000000..3e8619a9190 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts @@ -0,0 +1,390 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; +import { IWordAtPosition, getWordAtText } from 'vs/editor/common/core/wordHelper'; +import { CompletionContext, CompletionItem, CompletionItemKind } from 'vs/editor/common/languages'; +import { ITextModel } from 'vs/editor/common/model'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { localize } from 'vs/nls'; +import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { SubmitAction } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions'; +import { IChatWidget, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; +import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart'; +import { SelectAndInsertFileAction } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables'; +import { ChatAgentLocation, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestTextPart, ChatRequestVariablePart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; +import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; + +class SlashCommandCompletions extends Disposable { + constructor( + @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, + @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, + @IChatSlashCommandService private readonly chatSlashCommandService: IChatSlashCommandService + ) { + super(); + + this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { + _debugDisplayName: 'globalSlashCommands', + triggerCharacters: ['/'], + provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { + const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); + if (!widget || !widget.viewModel || widget.location !== ChatAgentLocation.Panel /* TODO@jrieken - enable when agents are adopted*/) { + return null; + } + + const range = computeCompletionRanges(model, position, /\/\w*/g); + if (!range) { + return null; + } + + const parsedRequest = widget.parsedInput.parts; + const usedAgent = parsedRequest.find(p => p instanceof ChatRequestAgentPart); + if (usedAgent) { + // No (classic) global slash commands when an agent is used + return; + } + + const slashCommands = this.chatSlashCommandService.getCommands(); + if (!slashCommands) { + return null; + } + + return { + suggestions: slashCommands.map((c, i): CompletionItem => { + const withSlash = `/${c.command}`; + return { + label: withSlash, + insertText: c.executeImmediately ? '' : `${withSlash} `, + detail: c.detail, + range: new Range(1, 1, 1, 1), + sortText: c.sortText ?? 'a'.repeat(i + 1), + kind: CompletionItemKind.Text, // The icons are disabled here anyway, + command: c.executeImmediately ? { id: SubmitAction.ID, title: withSlash, arguments: [{ widget, inputValue: `${withSlash} ` }] } : undefined, + }; + }) + }; + } + })); + } +} + +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(SlashCommandCompletions, LifecyclePhase.Eventually); + +class AgentCompletions extends Disposable { + constructor( + @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, + @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, + @IChatAgentService private readonly chatAgentService: IChatAgentService, + ) { + super(); + + this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { + _debugDisplayName: 'chatAgent', + triggerCharacters: ['@'], + provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { + const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); + if (!widget || !widget.viewModel || widget.location !== ChatAgentLocation.Panel /* TODO@jrieken - enable when agents are adopted*/) { + return null; + } + + const parsedRequest = widget.parsedInput.parts; + const usedAgent = parsedRequest.find(p => p instanceof ChatRequestAgentPart); + if (usedAgent && !Range.containsPosition(usedAgent.editorRange, position)) { + // Only one agent allowed + return; + } + + const range = computeCompletionRanges(model, position, /@\w*/g); + if (!range) { + return null; + } + + const agents = this.chatAgentService.getAgents() + .filter(a => !a.isDefault) + .filter(a => a.locations.includes(widget.location)); + + return { + suggestions: agents.map((a, i): CompletionItem => { + const withAt = `@${a.name}`; + const isDupe = !!agents.find(other => other.name === a.name && other.id !== a.id); + return { + // Leading space is important because detail has no space at the start by design + label: isDupe ? + { label: withAt, description: a.description, detail: ` (${a.publisherDisplayName})` } : + withAt, + insertText: `${withAt} `, + 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 + }; + }) + }; + } + })); + + this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { + _debugDisplayName: 'chatAgentSubcommand', + triggerCharacters: ['/'], + provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken) => { + const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); + if (!widget || !widget.viewModel || widget.location !== ChatAgentLocation.Panel /* TODO@jrieken - enable when agents are adopted*/) { + return; + } + + const range = computeCompletionRanges(model, position, /\/\w*/g); + if (!range) { + return null; + } + + const parsedRequest = widget.parsedInput.parts; + const usedAgentIdx = parsedRequest.findIndex((p): p is ChatRequestAgentPart => p instanceof ChatRequestAgentPart); + if (usedAgentIdx < 0) { + return; + } + + const usedSubcommand = parsedRequest.find(p => p instanceof ChatRequestAgentSubcommandPart); + if (usedSubcommand) { + // Only one allowed + return; + } + + for (const partAfterAgent of parsedRequest.slice(usedAgentIdx + 1)) { + // Could allow text after 'position' + if (!(partAfterAgent instanceof ChatRequestTextPart) || !partAfterAgent.text.trim().match(/^(\/\w*)?$/)) { + // No text allowed between agent and subcommand + return; + } + } + + const usedAgent = parsedRequest[usedAgentIdx] as ChatRequestAgentPart; + return { + suggestions: usedAgent.agent.slashCommands.map((c, i): CompletionItem => { + const withSlash = `/${c.name}`; + return { + label: withSlash, + insertText: `${withSlash} `, + detail: c.description, + range, + kind: CompletionItemKind.Text, // The icons are disabled here anyway + }; + }) + }; + } + })); + + // list subcommands when the query is empty, insert agent+subcommand + this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { + _debugDisplayName: 'chatAgentAndSubcommand', + triggerCharacters: ['/'], + provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken) => { + const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); + const viewModel = widget?.viewModel; + if (!widget || !viewModel || widget.location !== ChatAgentLocation.Panel /* TODO@jrieken - enable when agents are adopted*/) { + return; + } + + const range = computeCompletionRanges(model, position, /\/\w*/g); + if (!range) { + return null; + } + + const agents = this.chatAgentService.getAgents() + .filter(a => a.locations.includes(widget.location)); + + const justAgents: CompletionItem[] = agents + .filter(a => !a.isDefault) + .map(agent => { + const isDupe = !!agents.find(other => other.name === agent.name && other.id !== agent.id); + const detail = agent.description; + const agentLabel = `${chatAgentLeader}${agent.name}`; + + return { + label: isDupe ? + { label: agentLabel, description: agent.description, detail: ` (${agent.publisherDisplayName})` } : + agentLabel, + detail, + filterText: `${chatSubcommandLeader}${agent.name}`, + insertText: `${agentLabel} `, + range: new Range(1, 1, 1, 1), + kind: CompletionItemKind.Text, + sortText: `${chatSubcommandLeader}${agent.id}`, + command: { id: AssignSelectedAgentAction.ID, title: AssignSelectedAgentAction.ID, arguments: [{ agent, widget } satisfies AssignSelectedAgentActionArgs] }, + }; + }); + + return { + suggestions: justAgents.concat( + agents.flatMap(agent => agent.slashCommands.map((c, i) => { + const agentLabel = `${chatAgentLeader}${agent.name}`; + const withSlash = `${chatSubcommandLeader}${c.name}`; + return { + label: { label: withSlash, description: agentLabel }, + 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}`, + command: { id: AssignSelectedAgentAction.ID, title: AssignSelectedAgentAction.ID, arguments: [{ agent, widget } satisfies AssignSelectedAgentActionArgs] }, + } satisfies CompletionItem; + }))) + }; + } + })); + } +} +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 + + constructor( + @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, + @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, + ) { + super(); + + this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { + _debugDisplayName: 'chatDynamicCompletions', + triggerCharacters: [chatVariableLeader], + provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { + const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); + if (!widget || !widget.supportsFileReferences || widget.location !== ChatAgentLocation.Panel /* TODO@jrieken - enable when agents are adopted*/) { + return null; + } + + const range = computeCompletionRanges(model, position, BuiltinDynamicCompletions.VariableNameDef); + if (!range) { + return null; + } + + const afterRange = new Range(position.lineNumber, range.replace.startColumn, position.lineNumber, range.replace.startColumn + '#file:'.length); + return { + suggestions: [ + { + label: `${chatVariableLeader}file`, + insertText: `${chatVariableLeader}file:`, + detail: localize('pickFileLabel', "Pick a file"), + range, + kind: CompletionItemKind.Text, + command: { id: SelectAndInsertFileAction.ID, title: SelectAndInsertFileAction.ID, arguments: [{ widget, range: afterRange }] }, + sortText: 'z' + } satisfies CompletionItem + ] + }; + } + })); + } +} + +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(BuiltinDynamicCompletions, LifecyclePhase.Eventually); + +function computeCompletionRanges(model: ITextModel, position: Position, reg: RegExp): { insert: Range; replace: Range; varWord: IWordAtPosition | null } | undefined { + const varWord = getWordAtText(position.column, reg, model.getLineContent(position.lineNumber), 0); + if (!varWord && model.getWordUntilPosition(position).word) { + // inside a "normal" word + return; + } + + let insert: Range; + let replace: Range; + if (!varWord) { + insert = replace = Range.fromPositions(position); + } else { + insert = new Range(position.lineNumber, varWord.startColumn, position.lineNumber, position.column); + replace = new Range(position.lineNumber, varWord.startColumn, position.lineNumber, varWord.endColumn); + } + + return { insert, replace, varWord }; +} + +class VariableCompletions extends Disposable { + + private static readonly VariableNameDef = new RegExp(`${chatVariableLeader}\\w*`, 'g'); // MUST be using `g`-flag + + constructor( + @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, + @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, + @IChatVariablesService private readonly chatVariablesService: IChatVariablesService, + ) { + super(); + + this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { + _debugDisplayName: 'chatVariables', + triggerCharacters: [chatVariableLeader], + provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { + + const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); + if (!widget || widget.location !== ChatAgentLocation.Panel /* TODO@jrieken - enable when agents are adopted*/) { + return null; + } + + const range = computeCompletionRanges(model, position, VariableCompletions.VariableNameDef); + if (!range) { + return null; + } + + const usedVariables = widget.parsedInput.parts.filter((p): p is ChatRequestVariablePart => p instanceof ChatRequestVariablePart); + const variableItems = Array.from(this.chatVariablesService.getVariables()) + // This doesn't look at dynamic variables like `file`, where multiple makes sense. + .filter(v => !usedVariables.some(usedVar => usedVar.variableName === v.name)) + .map((v): CompletionItem => { + const withLeader = `${chatVariableLeader}${v.name}`; + return { + label: withLeader, + range, + insertText: withLeader + ' ', + detail: v.description, + kind: CompletionItemKind.Text, // The icons are disabled here anyway + sortText: 'z' + }; + }); + + return { + suggestions: variableItems + }; + } + })); + } +} + +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(VariableCompletions, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts index 30fd0e2f170..bafc22d77ee 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts @@ -3,36 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -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'; import { Range } from 'vs/editor/common/core/range'; -import { IWordAtPosition, getWordAtText } from 'vs/editor/common/core/wordHelper'; import { IDecorationOptions } from 'vs/editor/common/editorCommon'; -import { CompletionContext, CompletionItem, CompletionItemKind } from 'vs/editor/common/languages'; -import { ITextModel } from 'vs/editor/common/model'; -import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; -import { localize } from 'vs/nls'; -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 { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { inputPlaceholderForeground } from 'vs/platform/theme/common/colorRegistry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; -import { SubmitAction } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions'; -import { IChatWidget, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; -import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart'; +import { IChatWidget } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget'; -import { SelectAndInsertFileAction, dynamicVariableDecorationType } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables'; +import { dynamicVariableDecorationType } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables'; import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { chatSlashCommandBackground, chatSlashCommandForeground } from 'vs/workbench/contrib/chat/common/chatColors'; -import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, ChatRequestTextPart, ChatRequestVariablePart, IParsedChatRequestPart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, ChatRequestTextPart, ChatRequestVariablePart, IParsedChatRequestPart, chatAgentLeader, chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; -import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; -import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; -import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; const decorationDescription = 'chat'; const placeholderDecorationType = 'chat-session-detail'; @@ -266,369 +251,6 @@ class InputEditorSlashCommandMode extends Disposable { ChatWidget.CONTRIBS.push(InputEditorDecorations, InputEditorSlashCommandMode); -class SlashCommandCompletions extends Disposable { - constructor( - @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, - @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, - @IChatSlashCommandService private readonly chatSlashCommandService: IChatSlashCommandService - ) { - super(); - - this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { - _debugDisplayName: 'globalSlashCommands', - triggerCharacters: ['/'], - provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { - const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); - if (!widget || !widget.viewModel || widget.location !== ChatAgentLocation.Panel /* TODO@jrieken - enable when agents are adopted*/) { - return null; - } - - const range = computeCompletionRanges(model, position, /\/\w*/g); - if (!range) { - return null; - } - - const parsedRequest = widget.parsedInput.parts; - const usedAgent = parsedRequest.find(p => p instanceof ChatRequestAgentPart); - if (usedAgent) { - // No (classic) global slash commands when an agent is used - return; - } - - const slashCommands = this.chatSlashCommandService.getCommands(); - if (!slashCommands) { - return null; - } - - return { - suggestions: slashCommands.map((c, i): CompletionItem => { - const withSlash = `/${c.command}`; - return { - label: withSlash, - insertText: c.executeImmediately ? '' : `${withSlash} `, - detail: c.detail, - range: new Range(1, 1, 1, 1), - sortText: c.sortText ?? 'a'.repeat(i + 1), - kind: CompletionItemKind.Text, // The icons are disabled here anyway, - command: c.executeImmediately ? { id: SubmitAction.ID, title: withSlash, arguments: [{ widget, inputValue: `${withSlash} ` }] } : undefined, - }; - }) - }; - } - })); - } -} - -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(SlashCommandCompletions, LifecyclePhase.Eventually); - -class AgentCompletions extends Disposable { - constructor( - @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, - @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, - @IChatAgentService private readonly chatAgentService: IChatAgentService, - ) { - super(); - - this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { - _debugDisplayName: 'chatAgent', - triggerCharacters: ['@'], - provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { - const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); - if (!widget || !widget.viewModel || widget.location !== ChatAgentLocation.Panel /* TODO@jrieken - enable when agents are adopted*/) { - return null; - } - - const parsedRequest = widget.parsedInput.parts; - const usedAgent = parsedRequest.find(p => p instanceof ChatRequestAgentPart); - if (usedAgent && !Range.containsPosition(usedAgent.editorRange, position)) { - // Only one agent allowed - return; - } - - const range = computeCompletionRanges(model, position, /@\w*/g); - if (!range) { - return null; - } - - const agents = this.chatAgentService.getAgents() - .filter(a => !a.isDefault) - .filter(a => a.locations.includes(widget.location)); - - return { - suggestions: agents.map((a, i): CompletionItem => { - const withAt = `@${a.name}`; - const isDupe = !!agents.find(other => other.name === a.name && other.id !== a.id); - return { - // Leading space is important because detail has no space at the start by design - label: isDupe ? - { label: withAt, description: a.description, detail: ` (${a.publisherDisplayName})` } : - withAt, - insertText: `${withAt} `, - 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 - }; - }) - }; - } - })); - - this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { - _debugDisplayName: 'chatAgentSubcommand', - triggerCharacters: ['/'], - provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken) => { - const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); - if (!widget || !widget.viewModel || widget.location !== ChatAgentLocation.Panel /* TODO@jrieken - enable when agents are adopted*/) { - return; - } - - const range = computeCompletionRanges(model, position, /\/\w*/g); - if (!range) { - return null; - } - - const parsedRequest = widget.parsedInput.parts; - const usedAgentIdx = parsedRequest.findIndex((p): p is ChatRequestAgentPart => p instanceof ChatRequestAgentPart); - if (usedAgentIdx < 0) { - return; - } - - const usedSubcommand = parsedRequest.find(p => p instanceof ChatRequestAgentSubcommandPart); - if (usedSubcommand) { - // Only one allowed - return; - } - - for (const partAfterAgent of parsedRequest.slice(usedAgentIdx + 1)) { - // Could allow text after 'position' - if (!(partAfterAgent instanceof ChatRequestTextPart) || !partAfterAgent.text.trim().match(/^(\/\w*)?$/)) { - // No text allowed between agent and subcommand - return; - } - } - - const usedAgent = parsedRequest[usedAgentIdx] as ChatRequestAgentPart; - return { - suggestions: usedAgent.agent.slashCommands.map((c, i): CompletionItem => { - const withSlash = `/${c.name}`; - return { - label: withSlash, - insertText: `${withSlash} `, - detail: c.description, - range, - kind: CompletionItemKind.Text, // The icons are disabled here anyway - }; - }) - }; - } - })); - - // list subcommands when the query is empty, insert agent+subcommand - this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { - _debugDisplayName: 'chatAgentAndSubcommand', - triggerCharacters: ['/'], - provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken) => { - const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); - const viewModel = widget?.viewModel; - if (!widget || !viewModel || widget.location !== ChatAgentLocation.Panel /* TODO@jrieken - enable when agents are adopted*/) { - return; - } - - const range = computeCompletionRanges(model, position, /\/\w*/g); - if (!range) { - return null; - } - - const agents = this.chatAgentService.getAgents() - .filter(a => a.locations.includes(widget.location)); - - const justAgents: CompletionItem[] = agents - .filter(a => !a.isDefault) - .map(agent => { - const isDupe = !!agents.find(other => other.name === agent.name && other.id !== agent.id); - const detail = agent.description; - const agentLabel = `${chatAgentLeader}${agent.name}`; - - return { - label: isDupe ? - { label: agentLabel, description: agent.description, detail: ` (${agent.publisherDisplayName})` } : - agentLabel, - detail, - filterText: `${chatSubcommandLeader}${agent.name}`, - insertText: `${agentLabel} `, - range: new Range(1, 1, 1, 1), - kind: CompletionItemKind.Text, - sortText: `${chatSubcommandLeader}${agent.id}`, - command: { id: AssignSelectedAgentAction.ID, title: AssignSelectedAgentAction.ID, arguments: [{ agent, widget } satisfies AssignSelectedAgentActionArgs] }, - }; - }); - - return { - suggestions: justAgents.concat( - agents.flatMap(agent => agent.slashCommands.map((c, i) => { - const agentLabel = `${chatAgentLeader}${agent.name}`; - const withSlash = `${chatSubcommandLeader}${c.name}`; - return { - label: { label: withSlash, description: agentLabel }, - 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}`, - command: { id: AssignSelectedAgentAction.ID, title: AssignSelectedAgentAction.ID, arguments: [{ agent, widget } satisfies AssignSelectedAgentActionArgs] }, - } satisfies CompletionItem; - }))) - }; - } - })); - } -} -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 - - constructor( - @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, - @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, - ) { - super(); - - this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { - _debugDisplayName: 'chatDynamicCompletions', - triggerCharacters: [chatVariableLeader], - provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { - const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); - if (!widget || !widget.supportsFileReferences || widget.location !== ChatAgentLocation.Panel /* TODO@jrieken - enable when agents are adopted*/) { - return null; - } - - const range = computeCompletionRanges(model, position, BuiltinDynamicCompletions.VariableNameDef); - if (!range) { - return null; - } - - const afterRange = new Range(position.lineNumber, range.replace.startColumn, position.lineNumber, range.replace.startColumn + '#file:'.length); - return { - suggestions: [ - { - label: `${chatVariableLeader}file`, - insertText: `${chatVariableLeader}file:`, - detail: localize('pickFileLabel', "Pick a file"), - range, - kind: CompletionItemKind.Text, - command: { id: SelectAndInsertFileAction.ID, title: SelectAndInsertFileAction.ID, arguments: [{ widget, range: afterRange }] }, - sortText: 'z' - } satisfies CompletionItem - ] - }; - } - })); - } -} - -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(BuiltinDynamicCompletions, LifecyclePhase.Eventually); - -function computeCompletionRanges(model: ITextModel, position: Position, reg: RegExp): { insert: Range; replace: Range; varWord: IWordAtPosition | null } | undefined { - const varWord = getWordAtText(position.column, reg, model.getLineContent(position.lineNumber), 0); - if (!varWord && model.getWordUntilPosition(position).word) { - // inside a "normal" word - return; - } - - let insert: Range; - let replace: Range; - if (!varWord) { - insert = replace = Range.fromPositions(position); - } else { - insert = new Range(position.lineNumber, varWord.startColumn, position.lineNumber, position.column); - replace = new Range(position.lineNumber, varWord.startColumn, position.lineNumber, varWord.endColumn); - } - - return { insert, replace, varWord }; -} - -class VariableCompletions extends Disposable { - - private static readonly VariableNameDef = new RegExp(`${chatVariableLeader}\\w*`, 'g'); // MUST be using `g`-flag - - constructor( - @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, - @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, - @IChatVariablesService private readonly chatVariablesService: IChatVariablesService, - ) { - super(); - - this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { - _debugDisplayName: 'chatVariables', - triggerCharacters: [chatVariableLeader], - provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { - - const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); - if (!widget || widget.location !== ChatAgentLocation.Panel /* TODO@jrieken - enable when agents are adopted*/) { - return null; - } - - const range = computeCompletionRanges(model, position, VariableCompletions.VariableNameDef); - if (!range) { - return null; - } - - const usedVariables = widget.parsedInput.parts.filter((p): p is ChatRequestVariablePart => p instanceof ChatRequestVariablePart); - const variableItems = Array.from(this.chatVariablesService.getVariables()) - // This doesn't look at dynamic variables like `file`, where multiple makes sense. - .filter(v => !usedVariables.some(usedVar => usedVar.variableName === v.name)) - .map((v): CompletionItem => { - const withLeader = `${chatVariableLeader}${v.name}`; - return { - label: withLeader, - range, - insertText: withLeader + ' ', - detail: v.description, - kind: CompletionItemKind.Text, // The icons are disabled here anyway - sortText: 'z' - }; - }); - - return { - suggestions: variableItems - }; - } - })); - } -} - -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(VariableCompletions, LifecyclePhase.Eventually); - class ChatTokenDeleter extends Disposable { public readonly id = 'chatTokenDeleter'; From 8d268f9b021ecc60edbda68b16c873f2f9aa87ef Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 8 May 2024 19:08:54 -0700 Subject: [PATCH 063/357] Fix blinking "used references" --- src/vs/workbench/contrib/chat/browser/chatListRenderer.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index ad2d8e5564b..27e16b3f8b3 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -800,11 +800,14 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer Date: Wed, 8 May 2024 19:09:36 -0700 Subject: [PATCH 064/357] Fix bad spacing with some markdown lists --- src/vs/workbench/contrib/chat/browser/media/chat.css | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index e52597277ed..c2345624344 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -263,6 +263,10 @@ margin: 0 0 16px 0; } +.interactive-item-container .value > .rendered-markdown li > p { + margin: 0; +} + .interactive-item-container .value .rendered-markdown ul { padding-inline-start: 24px; } @@ -324,6 +328,10 @@ margin: 0 0 8px 0; } +.interactive-item-container.interactive-item-compact .value > .rendered-markdown li > p { + margin: 0; +} + .interactive-item-container.interactive-item-compact .value .rendered-markdown h1 { margin: 8px 0; @@ -337,10 +345,6 @@ margin: 8px 0; } -.interactive-item-container.interactive-item-compact .value .rendered-markdown p { - margin: 0 0 8px 0; -} - .interactive-session .interactive-input-and-execute-toolbar { display: flex; box-sizing: border-box; From 5f78b58b57b7cf84d28d801fed6bb4a48f908601 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 8 May 2024 19:27:38 -0700 Subject: [PATCH 065/357] Support agent hover and proper rendering in /help (#212311) For microsoft/vscode-copilot-release#1166 --- .../contrib/chat/browser/chat.contribution.ts | 24 ++- .../contrib/chat/browser/chatAgentHover.ts | 2 +- .../chatMarkdownDecorationsRenderer.ts | 146 +++++++++++++++--- .../contrib/chat/browser/media/chat.css | 9 ++ .../contrib/chat/common/chatService.ts | 1 + .../contrib/chat/common/chatServiceImpl.ts | 5 +- 6 files changed, 147 insertions(+), 40 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index de2ed8deb5f..424f74b37c9 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -8,6 +8,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { isMacintosh } from 'vs/base/common/platform'; import * as nls from 'vs/nls'; +import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; @@ -17,11 +18,12 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { EditorPaneDescriptor, IEditorPaneRegistry } from 'vs/workbench/browser/editor'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; import { EditorExtensions, IEditorFactoryRegistry } from 'vs/workbench/common/editor'; +import { ChatAccessibilityHelp } from 'vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp'; import { registerChatActions } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; import { ACTION_ID_NEW_CHAT, registerNewChatActions } from 'vs/workbench/contrib/chat/browser/actions/chatClearActions'; import { registerChatCodeBlockActions, registerChatCodeCompareBlockActions } from 'vs/workbench/contrib/chat/browser/actions/chatCodeblockActions'; import { registerChatCopyActions } from 'vs/workbench/contrib/chat/browser/actions/chatCopyActions'; -import { IChatExecuteActionContext, SubmitAction, registerChatExecuteActions } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions'; +import { SubmitAction, registerChatExecuteActions } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions'; import { registerChatFileTreeActions } from 'vs/workbench/contrib/chat/browser/actions/chatFileTreeActions'; import { registerChatExportActions } from 'vs/workbench/contrib/chat/browser/actions/chatImportExport'; import { registerMoveActions } from 'vs/workbench/contrib/chat/browser/actions/chatMoveActions'; @@ -31,14 +33,17 @@ import { IChatAccessibilityService, IChatCodeBlockContextProviderService, IChatW import { ChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chatAccessibilityService'; import { ChatEditor, IChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatEditor'; import { ChatEditorInput, ChatEditorInputSerializer } from 'vs/workbench/contrib/chat/browser/chatEditorInput'; +import { agentSlashCommandToMarkdown, agentToMarkdown } from 'vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer'; +import { ChatExtensionPointHandler } from 'vs/workbench/contrib/chat/browser/chatParticipantContributions'; import { QuickChatService } from 'vs/workbench/contrib/chat/browser/chatQuick'; +import { ChatResponseAccessibleView } from 'vs/workbench/contrib/chat/browser/chatResponseAccessibleView'; import { ChatVariablesService } from 'vs/workbench/contrib/chat/browser/chatVariables'; import { ChatWidgetService } from 'vs/workbench/contrib/chat/browser/chatWidget'; import { ChatCodeBlockContextProviderService } from 'vs/workbench/contrib/chat/browser/codeBlockContextProviderService'; import 'vs/workbench/contrib/chat/browser/contrib/chatHistoryVariables'; import 'vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib'; -import { ChatAgentLocation, ChatAgentService, IChatAgentService, IChatAgentNameService, ChatAgentNameService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import { ChatAgentLocation, ChatAgentNameService, ChatAgentService, IChatAgentNameService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { chatVariableLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { ChatService } from 'vs/workbench/contrib/chat/common/chatServiceImpl'; import { ChatSlashCommandService, IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; @@ -50,10 +55,6 @@ import { IVoiceChatService, VoiceChatService } from 'vs/workbench/contrib/chat/c import { IEditorResolverService, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import '../common/chatColors'; -import { ChatExtensionPointHandler } from 'vs/workbench/contrib/chat/browser/chatParticipantContributions'; -import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; -import { ChatResponseAccessibleView } from 'vs/workbench/contrib/chat/browser/chatResponseAccessibleView'; -import { ChatAccessibilityHelp } from 'vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp'; // Register configuration const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); @@ -182,16 +183,11 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable { .filter(a => a.id !== defaultAgent?.id) .filter(a => a.locations.includes(ChatAgentLocation.Panel)) .map(async a => { - const agentWithLeader = `${chatAgentLeader}${a.name}`; - const actionArg: IChatExecuteActionContext = { inputValue: `${agentWithLeader} ${a.metadata.sampleRequest ?? ''}` }; - const urlSafeArg = encodeURIComponent(JSON.stringify(actionArg)); const description = a.description ? `- ${a.description}` : ''; - const agentLine = `* [\`${agentWithLeader}\`](command:${SubmitAction.ID}?${urlSafeArg}) ${description}`; + const agentLine = `- ${agentToMarkdown(a, true, chatAgentService)} ${description}`; const commandText = a.slashCommands.map(c => { - const actionArg: IChatExecuteActionContext = { inputValue: `${agentWithLeader} ${chatSubcommandLeader}${c.name} ${c.sampleRequest ?? ''}` }; - const urlSafeArg = encodeURIComponent(JSON.stringify(actionArg)); const description = c.description ? `- ${c.description}` : ''; - return `\t* [\`${chatSubcommandLeader}${c.name}\`](command:${SubmitAction.ID}?${urlSafeArg}) ${description}`; + return `\t* ${agentSlashCommandToMarkdown(a, c, chatAgentService)} ${description}`; }).join('\n'); return (agentLine + '\n' + commandText).trim(); diff --git a/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts b/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts index 55e72653021..e3f11a02aaa 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts @@ -112,7 +112,7 @@ export class ChatAgentHover extends Disposable { let description = agent.description ?? ''; if (description) { - if (!description.match(/\. *$/)) { + if (!description.match(/[\.\?\!] *$/)) { description += '.'; } } diff --git a/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts index 078dffefec5..68bd10ded02 100644 --- a/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts @@ -14,14 +14,52 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ILabelService } from 'vs/platform/label/common/label'; import { ILogService } from 'vs/platform/log/common/log'; import { ChatAgentHover } from 'vs/workbench/contrib/chat/browser/chatAgentHover'; -import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { ChatRequestAgentPart, ChatRequestDynamicVariablePart, ChatRequestTextPart, IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import { IChatAgentCommand, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { chatAgentLeader, ChatRequestAgentPart, ChatRequestDynamicVariablePart, ChatRequestTextPart, chatSubcommandLeader, IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { contentRefUrl } from '../common/annotations'; import { IHoverService } from 'vs/platform/hover/browser/hover'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { Button } from 'vs/base/browser/ui/button/button'; +import { chatSlashCommandBackground, chatSlashCommandForeground } from 'vs/workbench/contrib/chat/common/chatColors'; +import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; +import { asCssVariable } from 'vs/platform/theme/common/colorUtils'; -const variableRefUrl = 'http://_vscodedecoration_'; -const agentRefUrl = 'http://_chatagent_'; +/** For rendering slash commands, variables */ +const decorationRefUrl = `http://_vscodedecoration_`; + +/** For rendering agent decorations with hover */ +const agentRefUrl = `http://_chatagent_`; + +/** For rendering agent decorations with hover */ +const agentSlashRefUrl = `http://_chatslash_`; + +export function agentToMarkdown(agent: IChatAgentData, isClickable: boolean, chatAgentService: IChatAgentService): string { + let text = `${chatAgentLeader}${agent.name}`; + const isDupe = agent && chatAgentService.getAgentsByName(agent.name).length > 1; + if (isDupe) { + text += ` (${agent.publisherDisplayName})`; + } + + const args: IAgentWidgetArgs = { agentId: agent.id, isClickable }; + return `[${text}](${agentRefUrl}?${encodeURIComponent(JSON.stringify(args))})`; +} + +interface IAgentWidgetArgs { + agentId: string; + isClickable?: boolean; +} + +export function agentSlashCommandToMarkdown(agent: IChatAgentData, command: IChatAgentCommand, chatAgentService: IChatAgentService): string { + const text = `${chatSubcommandLeader}${command.name}`; + const args: ISlashCommandWidgetArgs = { agentId: agent.id, command: command.name }; + return `[${text}](${agentSlashRefUrl}?${encodeURIComponent(JSON.stringify(args))})`; +} + +interface ISlashCommandWidgetArgs { + agentId: string; + command: string; +} export class ChatMarkdownDecorationsRenderer { constructor( @@ -31,6 +69,8 @@ export class ChatMarkdownDecorationsRenderer { @IChatAgentService private readonly chatAgentService: IChatAgentService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IHoverService private readonly hoverService: IHoverService, + @IChatService private readonly chatService: IChatService, + @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, ) { } convertParsedRequestToMarkdown(parsedRequest: IParsedChatRequest): string { @@ -39,13 +79,7 @@ export class ChatMarkdownDecorationsRenderer { if (part instanceof ChatRequestTextPart) { result += part.text; } else if (part instanceof ChatRequestAgentPart) { - let text = part.text; - const isDupe = this.chatAgentService.getAgentsByName(part.agent.name).length > 1; - if (isDupe) { - text += ` (${part.agent.publisherDisplayName})`; - } - - result += `[${text}](${agentRefUrl}?${encodeURIComponent(part.agent.id)})`; + result += agentToMarkdown(part.agent, false, this.chatAgentService); } else { const uri = part instanceof ChatRequestDynamicVariablePart && part.data instanceof URI ? part.data : @@ -55,7 +89,7 @@ export class ChatMarkdownDecorationsRenderer { ''; const text = part.text; - result += `[${text}](${variableRefUrl}?${title})`; + result += `[${text}](${decorationRefUrl}?${title})`; } } @@ -68,12 +102,33 @@ export class ChatMarkdownDecorationsRenderer { const href = a.getAttribute('data-href'); if (href) { if (href.startsWith(agentRefUrl)) { - const title = decodeURIComponent(href.slice(agentRefUrl.length + 1)); - a.parentElement!.replaceChild( - this.renderAgentWidget(a.textContent!, title, store), - a); - } else if (href.startsWith(variableRefUrl)) { - const title = decodeURIComponent(href.slice(variableRefUrl.length + 1)); + let args: IAgentWidgetArgs | undefined; + try { + args = JSON.parse(decodeURIComponent(href.slice(agentRefUrl.length + 1))); + } catch (e) { + this.logService.error('Invalid chat widget render data JSON', toErrorMessage(e)); + } + + if (args) { + a.parentElement!.replaceChild( + this.renderAgentWidget(a.textContent!, args, store), + a); + } + } else if (href.startsWith(agentSlashRefUrl)) { + let args: ISlashCommandWidgetArgs | undefined; + try { + args = JSON.parse(decodeURIComponent(href.slice(agentRefUrl.length + 1))); + } catch (e) { + this.logService.error('Invalid chat slash command render data JSON', toErrorMessage(e)); + } + + if (args) { + a.parentElement!.replaceChild( + this.renderSlashCommandWidget(a.textContent!, args, store), + a); + } + } else if (href.startsWith(decorationRefUrl)) { + const title = decodeURIComponent(href.slice(decorationRefUrl.length + 1)); a.parentElement!.replaceChild( this.renderResourceWidget(a.textContent!, title), a); @@ -88,17 +143,59 @@ export class ChatMarkdownDecorationsRenderer { return store; } - private renderAgentWidget(name: string, id: string, store: DisposableStore): HTMLElement { - const container = dom.$('span.chat-resource-widget', undefined, dom.$('span', undefined, name)); + private renderAgentWidget(name: string, args: IAgentWidgetArgs, store: DisposableStore): HTMLElement { + let container: HTMLElement; + if (args.isClickable) { + container = dom.$('span.chat-agent-widget'); + const agent = this.chatAgentService.getAgent(args.agentId); + const button = store.add(new Button(container, { + buttonBackground: asCssVariable(chatSlashCommandBackground), + buttonForeground: asCssVariable(chatSlashCommandForeground), + buttonHoverBackground: undefined + })); + button.label = name; + store.add(button.onDidClick(() => { + const widget = this.chatWidgetService.lastFocusedWidget; + if (!widget || !agent) { + return; + } + + this.chatService.sendRequest(widget.viewModel!.sessionId, agent.metadata.sampleRequest ?? '', { location: widget.location, agentId: agent.id }); + })); + } else { + container = this.renderResourceWidget(name, undefined); + } store.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('element'), container, () => { const hover = store.add(this.instantiationService.createInstance(ChatAgentHover)); - hover.setAgent(id); + hover.setAgent(args.agentId); return hover.domNode; })); return container; } + private renderSlashCommandWidget(name: string, args: ISlashCommandWidgetArgs, store: DisposableStore): HTMLElement { + const container = dom.$('span.chat-agent-widget.chat-command-widget'); + const agent = this.chatAgentService.getAgent(args.agentId); + const button = store.add(new Button(container, { + buttonBackground: asCssVariable(chatSlashCommandBackground), + buttonForeground: asCssVariable(chatSlashCommandForeground), + buttonHoverBackground: undefined + })); + button.label = name; + store.add(button.onDidClick(() => { + const widget = this.chatWidgetService.lastFocusedWidget; + if (!widget || !agent) { + return; + } + + const command = agent.slashCommands.find(c => c.name === args.command); + this.chatService.sendRequest(widget.viewModel!.sessionId, command?.sampleRequest ?? '', { location: widget.location, agentId: agent.id, slashCommand: args.command }); + })); + + return container; + } + private renderFileWidget(href: string, a: HTMLAnchorElement): void { // TODO this can be a nicer FileLabel widget with an icon. Do a simple link for now. const fullUri = URI.parse(href); @@ -125,10 +222,13 @@ export class ChatMarkdownDecorationsRenderer { } - private renderResourceWidget(name: string, title: string): HTMLElement { + private renderResourceWidget(name: string, title: string | undefined): HTMLElement { const container = dom.$('span.chat-resource-widget'); const alias = dom.$('span', undefined, name); - alias.title = title; + if (title) { + alias.title = title; + } + container.appendChild(alias); return container; } diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index e52597277ed..73fc65f034f 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -606,11 +606,20 @@ .interactive-item-container .chat-resource-widget { background-color: var(--vscode-chat-slashCommandBackground); color: var(--vscode-chat-slashCommandForeground); +} + +.interactive-item-container .chat-resource-widget, +.interactive-item-container .chat-agent-widget .monaco-button { border-radius: 4px; white-space: nowrap; padding: 1px 3px; } +.interactive-item-container .chat-agent-widget .monaco-text-button { + display: inline; + border: none; +} + .interactive-session .chat-used-context.chat-used-context-collapsed .chat-used-context-list { display: none; } diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 2664be3798c..505dd820eda 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -298,6 +298,7 @@ export interface IChatSendRequestOptions { /** The target agent ID can be specified with this property instead of using @ in 'message' */ agentId?: string; + slashCommand?: string; } export const IChatService = createDecorator('IChatService'); diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index 5f22e62a370..a43b8b2f1c4 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -25,7 +25,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ChatAgentLocation, IChatAgent, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatModel, ChatRequestModel, ChatWelcomeMessageModel, IChatModel, IChatRequestModel, IChatRequestVariableData, IChatRequestVariableEntry, IExportableChatData, ISerializableChatData, ISerializableChatsData, getHistoryEntriesFromModel, updateRanges } from 'vs/workbench/contrib/chat/common/chatModel'; -import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, IParsedChatRequest, chatAgentLeader, getPromptText } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, IParsedChatRequest, chatAgentLeader, chatSubcommandLeader, getPromptText } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; import { ChatCopyKind, IChatCompleteResponse, IChatDetail, IChatFollowup, IChatProgress, IChatSendRequestData, IChatSendRequestOptions, IChatService, IChatTransferredSessionData, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; @@ -485,7 +485,8 @@ export class ChatService extends Disposable implements IChatService { throw new Error(`Unknown agent: ${options.agentId}`); } parserContext = { selectedAgent: agent }; - request = `${chatAgentLeader}${agent.name} ${request}`; + const commandPart = options.slashCommand ? ` ${chatSubcommandLeader}${options.slashCommand}` : ''; + request = `${chatAgentLeader}${agent.name}${commandPart} ${request}`; } const parsedRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionId, request, location, parserContext); From 41f6f5ad6e9e92e04d102be8bc9b6658c9b845e2 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 8 May 2024 16:29:55 -0700 Subject: [PATCH 066/357] yea actually that doesn't work --- .../package.json | 4 +- .../src/coverageProvider.ts | 202 ++++++++++++------ .../src/extension.ts | 12 +- .../src/sourceMapStore.ts | 102 --------- .../src/testOutputScanner.ts | 128 +++++++++-- .../src/v8CoverageWrangling.test.ts | 106 +++++++++ .../src/v8CoverageWrangling.ts | 164 ++++++++++++++ .../tsconfig.json | 1 + .../vscode-selfhost-test-provider/yarn.lock | 23 +- test/unit/fullJsonStreamReporter.js | 2 +- 10 files changed, 538 insertions(+), 206 deletions(-) delete mode 100644 .vscode/extensions/vscode-selfhost-test-provider/src/sourceMapStore.ts create mode 100644 .vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.test.ts create mode 100644 .vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.ts diff --git a/.vscode/extensions/vscode-selfhost-test-provider/package.json b/.vscode/extensions/vscode-selfhost-test-provider/package.json index 80ecda4306f..ffca4e554a0 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/package.json +++ b/.vscode/extensions/vscode-selfhost-test-provider/package.json @@ -69,12 +69,12 @@ "test": "npx mocha --ui tdd 'out/*.test.js'" }, "devDependencies": { + "@types/mocha": "^10.0.6", "@types/node": "18.x" }, "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "ansi-styles": "^5.2.0", - "istanbul-to-vscode": "^2.0.1", - "v8-to-istanbul": "^9.2.0" + "istanbul-to-vscode": "^2.0.1" } } diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/coverageProvider.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/coverageProvider.ts index b27b69d3f53..8059a3d1e1f 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/coverageProvider.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/coverageProvider.ts @@ -3,104 +3,166 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { TraceMap } from '@jridgewell/trace-mapping'; import { IstanbulCoverageContext } from 'istanbul-to-vscode'; -import { fileURLToPath } from 'url'; import * as vscode from 'vscode'; -import { SourceMapStore } from './sourceMapStore'; -import v8ToIstanbul = require('v8-to-istanbul'); +import { SourceLocationMapper, SourceMapStore } from './testOutputScanner'; +import { ICoverageRange, IScriptCoverage, OffsetToPosition, RangeCoverageTracker } from './v8CoverageWrangling'; export const istanbulCoverageContext = new IstanbulCoverageContext(); -export interface ICoverageRange { - start: number; - end: number; - covered: boolean; -} - -export interface IV8FunctionCoverage { - functionName: string; - isBlockCoverage: boolean; - ranges: IV8CoverageRange[]; -} - -export interface IV8CoverageRange { - startOffset: number; - endOffset: number; - count: number; -} - -/** V8 Script coverage data */ -export interface IScriptCoverage { - scriptId: string; - url: string; - // Script source added by the runner the first time the script is emitted. - source?: string; - functions: IV8FunctionCoverage[]; -} - /** * Tracks coverage in per-script coverage mode. There are two modes of coverage - * in this extension: generic istanbul reports, and V8 reports from the runtime + * in this extension: generic istanbul reports, and reports from the runtime * sent before and after each test case executes. This handles the latter. */ export class PerTestCoverageTracker { private readonly scripts = new Map(); - constructor(private readonly maps: SourceMapStore) { } + constructor(private readonly maps: SourceMapStore,) {} - /** Adds new coverage data to the run, optionally for a test item. */ - public add(run: vscode.TestRun, coverage: IScriptCoverage, test?: vscode.TestItem) { - let script = this.scripts.get(coverage.scriptId); - if (!script) { - if (!coverage.source) { - throw new Error('expected to have source the first time a script is seen'); - } - - script = new Script(coverage.url, coverage.source, this.maps); - this.scripts.set(coverage.scriptId, script); + public add(coverage: IScriptCoverage, test?: vscode.TestItem) { + const script = this.scripts.get(coverage.scriptId); + if (script) { + return script.add(coverage, test); + } + // ignore internals and node_modules + if (!coverage.url.startsWith('file://') || coverage.url.includes('node_modules')) { + return; + } + if (!coverage.source) { + throw new Error('expected to have source the first time a script is seen'); } - return script.add(run, coverage, test); + const src = new Script(vscode.Uri.parse(coverage.url), coverage.source, this.maps); + this.scripts.set(coverage.scriptId, src); + src.add(coverage, test); + } + + public async report(run: vscode.TestRun) { + await Promise.all(Array.from(this.scripts.values()).map(s => s.report(run))); } } class Script { - private sourceMap?: Promise; - private originalContent?: Promise; + private converter: OffsetToPosition; + + /** Tracking the overall coverage for the file */ + private overall = new ScriptCoverageTracker(); + /** Range tracking per-test item */ + private readonly perItem = new Map(); constructor( - public readonly url: string, - private readonly source: string, + public readonly uri: vscode.Uri, + source: string, private readonly maps: SourceMapStore, ) { + this.converter = new OffsetToPosition(source); } - public async add(run: vscode.TestRun, coverage: IScriptCoverage, test?: vscode.TestItem) { - if (!coverage.url.startsWith('file://')) { - return; + public add(coverage: IScriptCoverage, test?: vscode.TestItem) { + this.overall.add(coverage); + if (test) { + const p = new ScriptCoverageTracker(); + p.add(coverage); + this.perItem.set(test, p); } + } - const sourceMap = await (this.sourceMap ??= this.maps.loadSourceMap(coverage.url)); - const originalSource = await (this.originalContent ??= this.maps.getSourceFileContents(coverage.url)); - const istanbuled = v8ToIstanbul(fileURLToPath(coverage.url), undefined, sourceMap && originalSource - ? { source: this.source, originalSource, sourceMap: { sourcemap: sourceMap } } - : { source: this.source } - ); + public async report(run: vscode.TestRun) { + const mapper = await this.maps.getSourceLocationMapper(this.uri.toString()); + const originalUri = await this.maps.getSourceFile(this.uri.toString()) || this.uri; - await istanbuled.load(); - - const coverages = await istanbulCoverageContext.fromJson(istanbuled.toIstanbul(), { - mapFileUri: uri => this.maps.getSourceFile(uri.toString()), - mapLocation: (uri, position) => - this.maps.getSourceLocation(uri.toString(), position.line, position.character), - }); - - for (const coverage of coverages) { - if (test) { - (coverage as vscode.FileCoverage2).testItem = test; - } - run.addCoverage(coverage); + run.addCoverage(this.overall.report(originalUri, this.converter, mapper)); + for (const [test, projection] of this.perItem) { + run.addCoverage(projection.report(originalUri, this.converter, mapper, test)); + } + } +} + +class ScriptCoverageTracker { + /** Range tracking for non-block coverage in the file */ + private file = new RangeCoverageTracker(); + /** Range tracking for block coverage in the file */ + private readonly blocks = new Map(); + + public add(coverage: IScriptCoverage) { + for (const fn of coverage.functions) { + if (fn.isBlockCoverage) { + const key = `${fn.ranges[0].startOffset}/${fn.ranges[0].endOffset}`; + const block = this.blocks.get(key); + if (block) { + for (let i = 1; i < fn.ranges.length; i++) { + block.setCovered(fn.ranges[i].startOffset, fn.ranges[i].endOffset, fn.ranges[i].count > 0); + } + } else { + this.blocks.set(key, RangeCoverageTracker.initializeBlock(fn.ranges)); + } + } else { + for (const range of fn.ranges) { + this.file.setCovered(range.startOffset, range.endOffset, range.count > 0); + } + } + } + } + + /** + * Generates the script's coverage for the test run. + * + * If a source location mapper is given, it assumes the `uri` is the mapped + * URI, and that any unmapped locations/outside the URI should be ignored. + */ + public report(uri: vscode.Uri, convert: OffsetToPosition, mapper: SourceLocationMapper | undefined, item?: vscode.TestItem): V8CoverageFile { + + const file = new V8CoverageFile(uri, item); + + async function handleBlock(range: ICoverageRange) { + const startLine = convert.getLineOfOffset(range.start); + const endLine = convert.getLineOfOffset(range.end); + for (let i = startLine; i <= endLine; i++) { + const start = new vscode.Position(i, i === startLine ? range.start - convert.lines[i] : 0); + const startMap = mapper?.(start.line, start.line); + const end = new vscode.Position(i, i === endLine ? range.end - convert.lines[i] : 0); + const endMap = startMap && mapper?.(end.line, end.line); + if (mapper && (!endMap || uri.toString().toLowerCase() !== endMap.uri.toString().toLowerCase())) { + return; + } + + const detail = new vscode.StatementCoverage(range.covered, startMap && endMap + ? new vscode.Range(startMap.range.start, endMap.range.end) + : new vscode.Range(start, end) + ); + + file.add(detail); + } + } + + for (const range of this.file) { + handleBlock(range); + } + + for (const block of this.blocks.values()) { + for (const range of block) { + handleBlock(range); + } + } + + return file; + } +} + +export class V8CoverageFile extends vscode.FileCoverage { + public details: vscode.StatementCoverage[] = []; + + constructor(uri: vscode.Uri, item?: vscode.TestItem) { + super(uri, { covered: 0, total: 0 }); + (this as vscode.FileCoverage2).testItem = item; + } + + public add(detail: vscode.StatementCoverage) { + this.details.push(detail); + this.statementCoverage.total++; + if (detail.executed) { + this.statementCoverage.covered++; } } } diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts index cda561e1325..c8b0d8f92f3 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts @@ -7,8 +7,9 @@ import { randomBytes } from 'crypto'; import { tmpdir } from 'os'; import * as path from 'path'; import * as vscode from 'vscode'; -import { istanbulCoverageContext } from './coverageProvider'; +import { V8CoverageFile } from './coverageProvider'; import { FailingDeepStrictEqualAssertFixer } from './failingDeepStrictEqualAssertFixer'; +import { FailureTracker } from './failureTracker'; import { registerSnapshotUpdate } from './snapshot'; import { scanTestOutput } from './testOutputScanner'; import { @@ -19,7 +20,6 @@ import { itemData, } from './testTree'; import { BrowserTestRunner, PlatformTestRunner, VSCodeTestRunner } from './vscodeTestRunner'; -import { FailureTracker } from './failureTracker'; const TEST_FILE_PATTERN = 'src/vs/**/*.{test,integrationTest}.ts'; @@ -183,7 +183,13 @@ export async function activate(context: vscode.ExtensionContext) { true ); - coverage.loadDetailedCoverage = istanbulCoverageContext.loadDetailedCoverage; + coverage.loadDetailedCoverage = async (_run, coverage) => { + if (coverage instanceof V8CoverageFile) { + return coverage.details; + } + + return []; + }; for (const [name, arg] of browserArgs) { const cfg = ctrl.createRunProfile( diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/sourceMapStore.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/sourceMapStore.ts deleted file mode 100644 index bc29b891fd2..00000000000 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/sourceMapStore.ts +++ /dev/null @@ -1,102 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { - GREATEST_LOWER_BOUND, - LEAST_UPPER_BOUND, - originalPositionFor, - TraceMap -} from '@jridgewell/trace-mapping'; -import * as vscode from 'vscode'; -import { getContentFromFilesystem } from './testTree'; - -const inlineSourcemapRe = /^\/\/# sourceMappingURL=data:application\/json;base64,(.+)/m; -const sourceMapBiases = [GREATEST_LOWER_BOUND, LEAST_UPPER_BOUND] as const; - -export class SourceMapStore { - private readonly cache = new Map>(); - - async getSourceLocation(fileUri: string, line: number, col = 1) { - const sourceMap = await this.loadSourceMap(fileUri); - if (!sourceMap) { - return undefined; - } - - for (const bias of sourceMapBiases) { - const position = originalPositionFor(sourceMap, { column: col, line: line + 1, bias }); - if (position.line !== null && position.column !== null && position.source !== null) { - return new vscode.Location( - this.completeSourceMapUrl(sourceMap, position.source), - new vscode.Position(position.line - 1, position.column) - ); - } - } - - return undefined; - } - - async getSourceFile(compiledUri: string) { - const sourceMap = await this.loadSourceMap(compiledUri); - if (!sourceMap) { - return undefined; - } - - if (sourceMap.sources[0]) { - return this.completeSourceMapUrl(sourceMap, sourceMap.sources[0]); - } - - for (const bias of sourceMapBiases) { - const position = originalPositionFor(sourceMap, { column: 0, line: 1, bias }); - if (position.source !== null) { - return this.completeSourceMapUrl(sourceMap, position.source); - } - } - - return undefined; - } - - async getSourceFileContents(compiledUri: string) { - const sourceUri = await this.getSourceFile(compiledUri); - return sourceUri ? getContentFromFilesystem(sourceUri) : undefined; - } - - private completeSourceMapUrl(sm: TraceMap, source: string) { - if (sm.sourceRoot) { - try { - return vscode.Uri.parse(new URL(source, sm.sourceRoot).toString()); - } catch { - // ignored - } - } - - return vscode.Uri.parse(source); - } - - public loadSourceMap(fileUri: string) { - const existing = this.cache.get(fileUri); - if (existing) { - return existing; - } - - const promise = (async () => { - try { - const contents = await getContentFromFilesystem(vscode.Uri.parse(fileUri)); - const sourcemapMatch = inlineSourcemapRe.exec(contents); - if (!sourcemapMatch) { - return; - } - - const decoded = Buffer.from(sourcemapMatch[1], 'base64').toString(); - return new TraceMap(decoded, fileUri); - } catch (e) { - console.warn(`Error parsing sourcemap for ${fileUri}: ${(e as Error).stack}`); - return; - } - })(); - - this.cache.set(fileUri, promise); - return promise; - } -} diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts index d0fec1fb606..fceb5c3549d 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts @@ -3,14 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { + GREATEST_LOWER_BOUND, + LEAST_UPPER_BOUND, + originalPositionFor, + TraceMap, +} from '@jridgewell/trace-mapping'; import * as styles from 'ansi-styles'; import { ChildProcessWithoutNullStreams } from 'child_process'; import * as vscode from 'vscode'; -import { IScriptCoverage, PerTestCoverageTracker, istanbulCoverageContext } from './coverageProvider'; +import { istanbulCoverageContext, PerTestCoverageTracker } from './coverageProvider'; import { attachTestMessageMetadata } from './metadata'; import { snapshotComment } from './snapshot'; -import { SourceMapStore } from './sourceMapStore'; +import { getContentFromFilesystem } from './testTree'; import { StreamSplitter } from './streamSplitter'; +import { IScriptCoverage } from './v8CoverageWrangling'; export const enum MochaEvent { Start = 'start', @@ -20,8 +27,8 @@ export const enum MochaEvent { End = 'end', // custom events: - CoverageInit = 'coverage init', - CoverageIncrement = 'coverage increment', + CoverageInit = 'coverageInit', + CoverageIncrement = 'coverageIncrement', } export interface IStartEvent { @@ -63,7 +70,7 @@ export interface IEndEvent { export interface ITestCoverageCoverage { file: string; fullTitle: string; - coverage: IScriptCoverage; + coverage: { result: IScriptCoverage[] }; } export type MochaEventTuple = @@ -72,7 +79,7 @@ export type MochaEventTuple = | [MochaEvent.Pass, IPassEvent] | [MochaEvent.Fail, IFailEvent] | [MochaEvent.End, IEndEvent] - | [MochaEvent.CoverageInit, IScriptCoverage] + | [MochaEvent.CoverageInit, { result: IScriptCoverage[] }] | [MochaEvent.CoverageIncrement, ITestCoverageCoverage]; const LF = '\n'.charCodeAt(0); @@ -166,9 +173,9 @@ export async function scanTestOutput( return prom; }; + let perTestCoverage: PerTestCoverageTracker | undefined; let lastTest: vscode.TestItem | undefined; let ranAnyTest = false; - let perTestCoverage: PerTestCoverageTracker | undefined; try { if (cancellation.isCancellationRequested) { @@ -231,7 +238,6 @@ export async function scanTestOutput( if (tcase) { lastTest = tcase; task.passed(tcase, evt[1].duration); - tests.delete(title); } } break; @@ -265,8 +271,6 @@ export async function scanTestOutput( return; } - tests.delete(id); - const hasDiff = actual !== undefined && expected !== undefined && @@ -316,14 +320,18 @@ export async function scanTestOutput( break; case MochaEvent.CoverageInit: perTestCoverage ??= new PerTestCoverageTracker(store); - enqueueExitBlocker(perTestCoverage.add(task, evt[1])); + for (const result of evt[1].result) { + perTestCoverage.add(result); + } break; case MochaEvent.CoverageIncrement: { const { fullTitle, coverage } = evt[1]; const tcase = tests.get(fullTitle); if (tcase) { perTestCoverage ??= new PerTestCoverageTracker(store); - enqueueExitBlocker(perTestCoverage.add(task, coverage, tcase)); + for (const result of coverage.result) { + perTestCoverage.add(result, tcase); + } } break; } @@ -331,6 +339,10 @@ export async function scanTestOutput( }); }); + if (perTestCoverage) { + enqueueExitBlocker(perTestCoverage.report(task)); + } + await Promise.all([...exitBlockers]); if (coverageDir) { @@ -408,6 +420,98 @@ const tryMakeMarkdown = (message: string) => { return new vscode.MarkdownString(lines.join('\n')); }; +const inlineSourcemapRe = /^\/\/# sourceMappingURL=data:application\/json;base64,(.+)/m; +const sourceMapBiases = [GREATEST_LOWER_BOUND, LEAST_UPPER_BOUND] as const; + +export type SourceLocationMapper = (line: number, col: number) => vscode.Location | undefined; + +export class SourceMapStore { + private readonly cache = new Map>(); + + async getSourceLocationMapper(fileUri: string) { + const sourceMap = await this.loadSourceMap(fileUri); + return (line: number, col: number) => { + if (!sourceMap) { + return undefined; + } + + for (const bias of sourceMapBiases) { + const position = originalPositionFor(sourceMap, { column: col, line: line + 1, bias }); + if (position.line !== null && position.column !== null && position.source !== null) { + return new vscode.Location( + this.completeSourceMapUrl(sourceMap, position.source), + new vscode.Position(position.line - 1, position.column) + ); + } + } + + return undefined; + }; + } + + async getSourceLocation(fileUri: string, line: number, col = 1) { + return this.getSourceLocationMapper(fileUri).then(m => m(line, col)); + } + + async getSourceFile(compiledUri: string) { + const sourceMap = await this.loadSourceMap(compiledUri); + if (!sourceMap) { + return undefined; + } + + if (sourceMap.sources[0]) { + return this.completeSourceMapUrl(sourceMap, sourceMap.sources[0]); + } + + for (const bias of sourceMapBiases) { + const position = originalPositionFor(sourceMap, { column: 0, line: 1, bias }); + if (position.source !== null) { + return this.completeSourceMapUrl(sourceMap, position.source); + } + } + + return undefined; + } + + private completeSourceMapUrl(sm: TraceMap, source: string) { + if (sm.sourceRoot) { + try { + return vscode.Uri.parse(new URL(source, sm.sourceRoot).toString()); + } catch { + // ignored + } + } + + return vscode.Uri.parse(source); + } + + private loadSourceMap(fileUri: string) { + const existing = this.cache.get(fileUri); + if (existing) { + return existing; + } + + const promise = (async () => { + try { + const contents = await getContentFromFilesystem(vscode.Uri.parse(fileUri)); + const sourcemapMatch = inlineSourcemapRe.exec(contents); + if (!sourcemapMatch) { + return; + } + + const decoded = Buffer.from(sourcemapMatch[1], 'base64').toString(); + return new TraceMap(decoded, fileUri); + } catch (e) { + console.warn(`Error parsing sourcemap for ${fileUri}: ${(e as Error).stack}`); + return; + } + })(); + + this.cache.set(fileUri, promise); + return promise; + } +} + const locationRe = /(file:\/{3}.+):([0-9]+):([0-9]+)/g; async function replaceAllLocations(store: SourceMapStore, str: string) { diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.test.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.test.ts new file mode 100644 index 00000000000..ad22e317860 --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.test.ts @@ -0,0 +1,106 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { RangeCoverageTracker } from './v8CoverageWrangling'; + +suite('v8CoverageWrangling', () => { + suite('RangeCoverageTracker', () => { + test('covers new range', () => { + const rt = new RangeCoverageTracker(); + rt.cover(5, 10); + assert.deepStrictEqual([...rt], [{ start: 5, end: 10, covered: true }]); + }); + + test('non overlapping ranges', () => { + const rt = new RangeCoverageTracker(); + rt.cover(5, 10); + rt.cover(15, 20); + assert.deepStrictEqual([...rt], [ + { start: 5, end: 10, covered: true }, + { start: 15, end: 20, covered: true }, + ]); + }); + + test('covers exact', () => { + const rt = new RangeCoverageTracker(); + rt.uncovered(5, 10); + rt.cover(5, 10); + assert.deepStrictEqual([...rt], [ + { start: 5, end: 10, covered: true }, + ]); + }); + + test('overlap at start', () => { + const rt = new RangeCoverageTracker(); + rt.uncovered(5, 10); + rt.cover(2, 7); + assert.deepStrictEqual([...rt], [ + { start: 2, end: 5, covered: true }, + { start: 5, end: 7, covered: true }, + { start: 7, end: 10, covered: false }, + ]); + }); + + test('overlap at end', () => { + const rt = new RangeCoverageTracker(); + rt.cover(5, 10); + rt.uncovered(2, 7); + assert.deepStrictEqual([...rt], [ + { start: 2, end: 5, covered: false }, + { start: 5, end: 7, covered: true }, + { start: 7, end: 10, covered: true }, + ]); + }); + + test('inner contained', () => { + const rt = new RangeCoverageTracker(); + rt.cover(5, 10); + rt.uncovered(2, 12); + assert.deepStrictEqual([...rt], [ + { start: 2, end: 5, covered: false }, + { start: 5, end: 10, covered: true }, + { start: 10, end: 12, covered: false }, + ]); + }); + + test('outer contained', () => { + const rt = new RangeCoverageTracker(); + rt.uncovered(5, 10); + rt.cover(7, 9); + assert.deepStrictEqual([...rt], [ + { start: 5, end: 7, covered: false }, + { start: 7, end: 9, covered: true }, + { start: 9, end: 10, covered: false }, + ]); + }); + + test('boundary touching', () => { + const rt = new RangeCoverageTracker(); + rt.uncovered(5, 10); + rt.cover(10, 15); + rt.uncovered(15, 20); + assert.deepStrictEqual([...rt], [ + { start: 5, end: 10, covered: false }, + { start: 10, end: 15, covered: true }, + { start: 15, end: 20, covered: false }, + ]); + }); + + test('initializeBlock', () => { + const rt = RangeCoverageTracker.initializeBlock([ + { count: 1, startOffset: 5, endOffset: 30 }, + { count: 1, startOffset: 8, endOffset: 10 }, + { count: 0, startOffset: 15, endOffset: 20 }, + ]); + + assert.deepStrictEqual([...rt], [ + { start: 5, end: 15, covered: true }, + { start: 15, end: 20, covered: false }, + { start: 20, end: 30, covered: true }, + ]); + }); + }); +}); diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.ts new file mode 100644 index 00000000000..9fbecbd8ba5 --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.ts @@ -0,0 +1,164 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export interface ICoverageRange { + start: number; + end: number; + covered: boolean; +} + +export interface IV8FunctionCoverage { + functionName: string; + isBlockCoverage: boolean; + ranges: IV8CoverageRange[]; +} + +export interface IV8CoverageRange { + startOffset: number; + endOffset: number; + count: number; +} + +/** V8 Script coverage data */ +export interface IScriptCoverage { + scriptId: string; + url: string; + // Script source added by the runner the first time the script is emitted. + source?: string; + functions: IV8FunctionCoverage[]; +} + + +export class RangeCoverageTracker implements Iterable { + /** + * A noncontiguous, non-overlapping, ordered set of ranges and whether + * that range has been covered. + */ + private ranges: readonly ICoverageRange[] = []; + + /** + * Adds a coverage tracker initialized for a function with {@link isBlockCoverage} set to true. + */ + public static initializeBlock(ranges: IV8CoverageRange[]) { + let start = ranges[0].startOffset; + const rt = new RangeCoverageTracker(); + if (!ranges[0].count) { + rt.uncovered(start, ranges[0].endOffset); + return rt; + } + + for (let i = 1; i < ranges.length; i++) { + const range = ranges[i]; + if (range.count) { + continue; + } + + rt.cover(start, range.startOffset); + rt.uncovered(range.startOffset, range.endOffset); + start = range.endOffset; + } + + rt.cover(start, ranges[0].endOffset); + return rt; + } + + /** Marks a range covered */ + public cover(start: number, end: number) { + this.setCovered(start, end, true); + } + + /** Marks a range as uncovered */ + public uncovered(start: number, end: number) { + this.setCovered(start, end, false); + } + + /** Iterates over coverage ranges */ + [Symbol.iterator]() { + return this.ranges[Symbol.iterator](); + } + + public setCovered(start: number, end: number, covered: boolean) { + const newRanges: ICoverageRange[] = []; + let i = 0; + for (; i < this.ranges.length && this.ranges[i].end <= start; i++) { + newRanges.push(this.ranges[i]); + } + + newRanges.push({ start, end, covered }); + for (; i < this.ranges.length; i++) { + const range = this.ranges[i]; + const last = newRanges[newRanges.length - 1]; + + if (range.start < last.start && range.end > last.end) { + // range contains last: + newRanges.pop(); + newRanges.push({ start: range.start, end: last.start, covered: range.covered }); + newRanges.push({ start: last.start, end: last.end, covered: range.covered || last.covered }); + newRanges.push({ start: last.end, end: range.end, covered: range.covered }); + } else if (range.start > last.start && range.end <= last.end) { + // last contains range: + newRanges.pop(); + newRanges.push({ start: last.start, end: range.start, covered: last.covered }); + newRanges.push({ start: range.start, end: range.end, covered: range.covered || last.covered }); + newRanges.push({ start: range.end, end: last.end, covered: last.covered }); + } else if (range.start < last.start && range.end <= last.end) { + // range overlaps start of last: + newRanges.pop(); + newRanges.push({ start: range.start, end: last.start, covered: range.covered }); + newRanges.push({ start: last.start, end: range.end, covered: range.covered || last.covered }); + newRanges.push({ start: range.end, end: last.end, covered: last.covered }); + } else if (range.start > last.start && range.end > last.end) { + // range overlaps end of last: + newRanges.pop(); + newRanges.push({ start: last.start, end: range.start, covered: last.covered }); + newRanges.push({ start: range.start, end: last.end, covered: range.covered || last.covered }); + newRanges.push({ start: last.end, end: range.end, covered: range.covered }); + } else { + // ranges are equal: + last.covered ||= range.covered; + } + } + + this.ranges = newRanges; + } +} + +export class OffsetToPosition { + /** Line numbers to byte offsets. */ + public readonly lines: number[] = []; + + constructor(public readonly source: string) { + this.lines.push(0); + for (let i = source.indexOf('\n'); i !== -1; i = source.indexOf('\n', i + 1)) { + this.lines.push(i + 1); + } + } + + /** + * Gets the line the offset appears on. + */ + public getLineOfOffset(offset: number): number { + let low = 0; + let high = this.lines.length; + while (low < high) { + const mid = Math.floor((low + high) / 2); + if (this.lines[mid] > offset) { + high = mid; + } else { + low = mid + 1; + } + } + + return low - 1; + } + + /** + * Converts from a file offset to a base 0 line/column . + */ + public convert(offset: number): { line: number; column: number } { + const line = this.getLineOfOffset(offset); + return { line: line, column: offset - this.lines[line] }; + } +} diff --git a/.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json b/.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json index 4ae0a106f09..0183a2ff57e 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json +++ b/.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json @@ -4,6 +4,7 @@ "outDir": "./out", "types": [ "node", + "mocha", ] }, "include": [ diff --git a/.vscode/extensions/vscode-selfhost-test-provider/yarn.lock b/.vscode/extensions/vscode-selfhost-test-provider/yarn.lock index 74a6f4caf5b..1117c1920dd 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/yarn.lock +++ b/.vscode/extensions/vscode-selfhost-test-provider/yarn.lock @@ -12,7 +12,7 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== -"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.25": +"@jridgewell/trace-mapping@^0.3.25": version "0.3.25" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== @@ -20,11 +20,16 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@types/istanbul-lib-coverage@^2.0.1", "@types/istanbul-lib-coverage@^2.0.6": +"@types/istanbul-lib-coverage@^2.0.6": version "2.0.6" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7" integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w== +"@types/mocha@^10.0.6": + version "10.0.6" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-10.0.6.tgz#818551d39113081048bdddbef96701b4e8bb9d1b" + integrity sha512-dJvrYWxP/UcXm36Qn36fxhUKu8A/xMRXVT2cliFF1Z7UA9liG5Psj3ezNSZw+5puH2czDXRLcXQxf8JbJt0ejg== + "@types/node@18.x": version "18.19.26" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.26.tgz#18991279d0a0e53675285e8cf4a0823766349729" @@ -37,11 +42,6 @@ ansi-styles@^5.2.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== -convert-source-map@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" - integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== - istanbul-to-vscode@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/istanbul-to-vscode/-/istanbul-to-vscode-2.0.1.tgz#84994d06e604b68cac7301840f338b1e74eb888b" @@ -53,12 +53,3 @@ undici-types@~5.26.4: version "5.26.5" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== - -v8-to-istanbul@^9.2.0: - version "9.2.0" - resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz#2ed7644a245cddd83d4e087b9b33b3e62dfd10ad" - integrity sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA== - dependencies: - "@jridgewell/trace-mapping" "^0.3.12" - "@types/istanbul-lib-coverage" "^2.0.1" - convert-source-map "^2.0.0" diff --git a/test/unit/fullJsonStreamReporter.js b/test/unit/fullJsonStreamReporter.js index c92870cb0d8..a130a36bf37 100644 --- a/test/unit/fullJsonStreamReporter.js +++ b/test/unit/fullJsonStreamReporter.js @@ -31,7 +31,7 @@ module.exports = class FullJsonStreamReporter extends BaseRunner { // custom coverage events: runner.on('coverage init', (c) => writeEvent(['coverageInit', c])); - runner.on('coverage increment', (context, c) => writeEvent(['coverageIncrement', context, c])); + runner.on('coverage increment', (context, coverage) => writeEvent(['coverageIncrement', { ...context, coverage }])); runner.on(EVENT_TEST_BEGIN, test => writeEvent(['testStart', clean(test)])); runner.on(EVENT_TEST_PASS, test => writeEvent(['pass', clean(test)])); From e8481cc5643f6dce1904e465ecd54dd36e0d7926 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 8 May 2024 21:08:24 -0700 Subject: [PATCH 067/357] Implement UI for reserved agent names --- .../api/browser/mainThreadChatAgents2.ts | 2 +- .../contrib/chat/browser/chat.contribution.ts | 4 +- .../contrib/chat/browser/chatAgentHover.ts | 10 +++- .../chatMarkdownDecorationsRenderer.ts | 54 +++++++++++-------- .../browser/contrib/chatInputCompletions.ts | 8 +-- .../contrib/chat/browser/media/chat.css | 1 - .../chat/browser/media/chatAgentHover.css | 12 ++++- .../contrib/chat/common/chatAgents.ts | 13 ++++- .../contrib/chat/common/chatRequestParser.ts | 10 +++- 9 files changed, 79 insertions(+), 35 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index 69dd832a312..57f98dd25bf 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -142,7 +142,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA description: dynamicProps.description, extensionId: extension, extensionDisplayName: extensionDescription?.displayName ?? extension.value, - extensionPublisherId: '', + extensionPublisherId: extensionDescription?.publisher ?? '', publisherDisplayName: dynamicProps.publisherDisplayName, metadata: revive(metadata), slashCommands: [], diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index f510430a4b6..256261d2150 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -185,10 +185,10 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable { .filter(a => a.locations.includes(ChatAgentLocation.Panel)) .map(async a => { const description = a.description ? `- ${a.description}` : ''; - const agentLine = `- ${agentToMarkdown(a, true, chatAgentService)} ${description}`; + const agentLine = `- ${agentToMarkdown(a, true)} ${description}`; const commandText = a.slashCommands.map(c => { const description = c.description ? `- ${c.description}` : ''; - return `\t* ${agentSlashCommandToMarkdown(a, c, chatAgentService)} ${description}`; + return `\t* ${agentSlashCommandToMarkdown(a, c)} ${description}`; }).join('\n'); return (agentLine + '\n' + commandText).trim(); diff --git a/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts b/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts index e3f11a02aaa..0459ac97b77 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts @@ -8,13 +8,14 @@ import { h } from 'vs/base/browser/dom'; import { Button } from 'vs/base/browser/ui/button/button'; import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Codicon } from 'vs/base/common/codicons'; import { Disposable } from 'vs/base/common/lifecycle'; import { FileAccess } from 'vs/base/common/network'; import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatAgentData, IChatAgentNameService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { showExtensionsWithIdsCommandId } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { verifiedPublisherIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; @@ -34,6 +35,7 @@ export class ChatAgentHover extends Disposable { @IChatAgentService private readonly chatAgentService: IChatAgentService, @IExtensionsWorkbenchService private readonly extensionService: IExtensionsWorkbenchService, @ICommandService private readonly commandService: ICommandService, + @IChatAgentNameService private readonly chatAgentNameService: IChatAgentNameService, ) { super(); @@ -51,6 +53,7 @@ export class ChatAgentHover extends Disposable { ]), ]), ]), + h('.chat-agent-hover-warning@warning'), h('span.chat-agent-hover-description@description'), h('span.chat-agent-hover-marketplace-button@button'), ]); @@ -71,6 +74,9 @@ export class ChatAgentHover extends Disposable { verifiedBadge, this.publisherName); + hoverElement.warning.appendChild(renderIcon(Codicon.warning)); + hoverElement.warning.appendChild(dom.$('span', undefined, localize('reservedName', "This chat extension is using a reserved name."))); + const label = localize('marketplaceLabel', "View in Marketplace") + '.'; const marketplaceButton = this._register(new Button(hoverElement.button, { title: label, @@ -118,6 +124,8 @@ export class ChatAgentHover extends Disposable { } this.description.textContent = description; + const isAllowed = this.chatAgentNameService.getAgentNameRestriction(agent).get(); + this.domNode.classList.toggle('allowedName', isAllowed); this.domNode.classList.toggle('verifiedPublisher', false); if (!agent.isDynamic) { diff --git a/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts index 68bd10ded02..9248c95f55f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts @@ -4,26 +4,26 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; +import { Button } from 'vs/base/browser/ui/button/button'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { revive } from 'vs/base/common/marshalling'; import { URI } from 'vs/base/common/uri'; import { Location } from 'vs/editor/common/languages'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ILabelService } from 'vs/platform/label/common/label'; import { ILogService } from 'vs/platform/log/common/log'; -import { ChatAgentHover } from 'vs/workbench/contrib/chat/browser/chatAgentHover'; -import { IChatAgentCommand, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { chatAgentLeader, ChatRequestAgentPart, ChatRequestDynamicVariablePart, ChatRequestTextPart, chatSubcommandLeader, IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { contentRefUrl } from '../common/annotations'; -import { IHoverService } from 'vs/platform/hover/browser/hover'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; -import { Button } from 'vs/base/browser/ui/button/button'; -import { chatSlashCommandBackground, chatSlashCommandForeground } from 'vs/workbench/contrib/chat/common/chatColors'; -import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; -import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { asCssVariable } from 'vs/platform/theme/common/colorUtils'; +import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; +import { ChatAgentHover } from 'vs/workbench/contrib/chat/browser/chatAgentHover'; +import { getFullyQualifiedId, IChatAgentCommand, IChatAgentData, IChatAgentNameService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { chatSlashCommandBackground, chatSlashCommandForeground } from 'vs/workbench/contrib/chat/common/chatColors'; +import { chatAgentLeader, ChatRequestAgentPart, ChatRequestDynamicVariablePart, ChatRequestTextPart, chatSubcommandLeader, IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; +import { contentRefUrl } from '../common/annotations'; /** For rendering slash commands, variables */ const decorationRefUrl = `http://_vscodedecoration_`; @@ -34,15 +34,9 @@ const agentRefUrl = `http://_chatagent_`; /** For rendering agent decorations with hover */ const agentSlashRefUrl = `http://_chatslash_`; -export function agentToMarkdown(agent: IChatAgentData, isClickable: boolean, chatAgentService: IChatAgentService): string { - let text = `${chatAgentLeader}${agent.name}`; - const isDupe = agent && chatAgentService.getAgentsByName(agent.name).length > 1; - if (isDupe) { - text += ` (${agent.publisherDisplayName})`; - } - +export function agentToMarkdown(agent: IChatAgentData, isClickable: boolean): string { const args: IAgentWidgetArgs = { agentId: agent.id, isClickable }; - return `[${text}](${agentRefUrl}?${encodeURIComponent(JSON.stringify(args))})`; + return `[${agent.name}](${agentRefUrl}?${encodeURIComponent(JSON.stringify(args))})`; } interface IAgentWidgetArgs { @@ -50,7 +44,7 @@ interface IAgentWidgetArgs { isClickable?: boolean; } -export function agentSlashCommandToMarkdown(agent: IChatAgentData, command: IChatAgentCommand, chatAgentService: IChatAgentService): string { +export function agentSlashCommandToMarkdown(agent: IChatAgentData, command: IChatAgentCommand): string { const text = `${chatSubcommandLeader}${command.name}`; const args: ISlashCommandWidgetArgs = { agentId: agent.id, command: command.name }; return `[${text}](${agentSlashRefUrl}?${encodeURIComponent(JSON.stringify(args))})`; @@ -71,6 +65,7 @@ export class ChatMarkdownDecorationsRenderer { @IHoverService private readonly hoverService: IHoverService, @IChatService private readonly chatService: IChatService, @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, + @IChatAgentNameService private readonly chatAgentNameService: IChatAgentNameService, ) { } convertParsedRequestToMarkdown(parsedRequest: IParsedChatRequest): string { @@ -79,7 +74,7 @@ export class ChatMarkdownDecorationsRenderer { if (part instanceof ChatRequestTextPart) { result += part.text; } else if (part instanceof ChatRequestAgentPart) { - result += agentToMarkdown(part.agent, false, this.chatAgentService); + result += agentToMarkdown(part.agent, false); } else { const uri = part instanceof ChatRequestDynamicVariablePart && part.data instanceof URI ? part.data : @@ -111,7 +106,7 @@ export class ChatMarkdownDecorationsRenderer { if (args) { a.parentElement!.replaceChild( - this.renderAgentWidget(a.textContent!, args, store), + this.renderAgentWidget(args, store), a); } } else if (href.startsWith(agentSlashRefUrl)) { @@ -143,11 +138,24 @@ export class ChatMarkdownDecorationsRenderer { return store; } - private renderAgentWidget(name: string, args: IAgentWidgetArgs, store: DisposableStore): HTMLElement { + private renderAgentWidget(args: IAgentWidgetArgs, store: DisposableStore): HTMLElement { + const agent = this.chatAgentService.getAgent(args.agentId); + let name: string; + if (agent) { + const isAllowed = this.chatAgentNameService.getAgentNameRestriction(agent).get(); + name = `${chatAgentLeader}${isAllowed ? agent.name : getFullyQualifiedId(agent)}`; + + const isDupe = isAllowed && this.chatAgentService.getAgentsByName(agent.name).length > 1; + if (isDupe) { + name += ` (${agent.publisherDisplayName})`; + } + } else { + name = args.agentId; + } + let container: HTMLElement; if (args.isClickable) { container = dom.$('span.chat-agent-widget'); - const agent = this.chatAgentService.getAgent(args.agentId); const button = store.add(new Button(container, { buttonBackground: asCssVariable(chatSlashCommandBackground), buttonForeground: asCssVariable(chatSlashCommandForeground), diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts index 3e8619a9190..75f517745e1 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts @@ -20,7 +20,7 @@ import { SubmitAction } from 'vs/workbench/contrib/chat/browser/actions/chatExec import { IChatWidget, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart'; import { SelectAndInsertFileAction } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables'; -import { ChatAgentLocation, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatAgentLocation, getFullyQualifiedId, IChatAgentData, IChatAgentNameService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestTextPart, ChatRequestVariablePart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; @@ -86,6 +86,7 @@ class AgentCompletions extends Disposable { @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, @IChatAgentService private readonly chatAgentService: IChatAgentService, + @IChatAgentNameService private readonly chatAgentNameService: IChatAgentNameService, ) { super(); @@ -116,8 +117,9 @@ class AgentCompletions extends Disposable { return { suggestions: agents.map((a, i): CompletionItem => { - const withAt = `@${a.name}`; - const isDupe = !!agents.find(other => other.name === a.name && other.id !== a.id); + const isAllowed = this.chatAgentNameService.getAgentNameRestriction(a).get(); + const withAt = `${chatAgentLeader}${isAllowed ? a.name : getFullyQualifiedId(a)}`; + const isDupe = isAllowed && !!agents.find(other => other.name === a.name && other.id !== a.id); return { // Leading space is important because detail has no space at the start by design label: isDupe ? diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 73fc65f034f..c3887037187 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -611,7 +611,6 @@ .interactive-item-container .chat-resource-widget, .interactive-item-container .chat-agent-widget .monaco-button { border-radius: 4px; - white-space: nowrap; padding: 1px 3px; } diff --git a/src/vs/workbench/contrib/chat/browser/media/chatAgentHover.css b/src/vs/workbench/contrib/chat/browser/media/chatAgentHover.css index 068ffd872ee..edbe3ab0cf6 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatAgentHover.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatAgentHover.css @@ -45,6 +45,15 @@ display: flex; } +.chat-agent-hover .chat-agent-hover-warning .codicon { + color: var(--vscode-notificationsWarningIcon-foreground) !important; + margin-right: 3px; +} + +.chat-agent-hover.allowedName .chat-agent-hover-warning { + display: none; +} + .chat-agent-hover-header .chat-agent-hover-name { font-size: 15px; font-weight: 600; @@ -65,7 +74,8 @@ } .chat-agent-hover-description, -.chat-agent-hover-marketplace-button .monaco-text-button { +.chat-agent-hover-marketplace-button .monaco-text-button, +.chat-agent-hover-warning { font-size: 13px; } diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index f303f0b56c6..9bf43b3db5c 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -155,6 +155,7 @@ export interface IChatAgentService { 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; getAgent(id: string): IChatAgentData | undefined; + getAgentByFullyQualifiedId(id: string): IChatAgentData | undefined; getAgents(): IChatAgentData[]; getActivatedAgents(): Array; getAgentsByName(name: string): IChatAgentData[]; @@ -186,7 +187,7 @@ export class ChatAgentService implements IChatAgentService { private readonly _hasDefaultAgent: IContextKey; constructor( - @IContextKeyService private readonly contextKeyService: IContextKeyService + @IContextKeyService private readonly contextKeyService: IContextKeyService, ) { this._hasDefaultAgent = CONTEXT_CHAT_ENABLED.bindTo(this.contextKeyService); } @@ -282,6 +283,10 @@ export class ChatAgentService implements IChatAgentService { return this._getAgentEntry(id)?.data; } + getAgentByFullyQualifiedId(id: string): IChatAgentData | undefined { + return this._agents.find(a => getFullyQualifiedId(a.data) === id)?.data; + } + /** * Returns all agent datas that exist- static registered and dynamic ones. */ @@ -447,6 +452,8 @@ export class ChatAgentNameService implements IChatAgentNameService { } getAgentNameRestriction(chatAgentData: IChatAgentData): IObservable { + // Registry is a map of name to an array of extension publisher IDs or extension IDs that are allowed to use it. + // Look up the list of extensions that are allowed to use this name const allowList = this.registry.map(registry => registry[chatAgentData.name.toLowerCase()]); return allowList.map(allowList => { if (!allowList) { @@ -461,3 +468,7 @@ export class ChatAgentNameService implements IChatAgentNameService { this.disposed = true; } } + +export function getFullyQualifiedId(chatAgentData: IChatAgentData): string { + return `${chatAgentData.extensionId.value}.${chatAgentData.id}`; +} diff --git a/src/vs/workbench/contrib/chat/common/chatRequestParser.ts b/src/vs/workbench/contrib/chat/common/chatRequestParser.ts index f60b9cb5dc3..d0aed8771d0 100644 --- a/src/vs/workbench/contrib/chat/common/chatRequestParser.ts +++ b/src/vs/workbench/contrib/chat/common/chatRequestParser.ts @@ -11,7 +11,7 @@ import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestDynami import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; import { IChatVariablesService, IDynamicVariable } from 'vs/workbench/contrib/chat/common/chatVariables'; -const agentReg = /^@([\w_\-]+)(?=(\s|$|\b))/i; // An @-agent +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 @@ -100,7 +100,13 @@ export class ChatRequestParser { const agentRange = new OffsetRange(offset, offset + full.length); const agentEditorRange = new Range(position.lineNumber, position.column, position.lineNumber, position.column + full.length); - const agents = this.agentService.getAgentsByName(name); + let agents = this.agentService.getAgentsByName(name); + if (!agents.length) { + const fqAgent = this.agentService.getAgentByFullyQualifiedId(name); + if (fqAgent) { + agents = [fqAgent]; + } + } // 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. From 9eff384b7799006696343d672afe4ce7b7aa774b Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 9 May 2024 12:07:26 +0200 Subject: [PATCH 068/357] feat: add language option to text-to-speech sessions (#212330) --- src/vs/workbench/api/browser/mainThreadSpeech.ts | 4 ++-- src/vs/workbench/api/common/extHost.protocol.ts | 2 +- src/vs/workbench/api/common/extHostSpeech.ts | 4 ++-- .../accessibility/browser/accessibilityConfiguration.ts | 2 +- src/vs/workbench/contrib/speech/browser/speechService.ts | 8 ++++++-- src/vs/workbench/contrib/speech/common/speechService.ts | 6 +++++- src/vscode-dts/vscode.proposed.speech.d.ts | 6 +++++- 7 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadSpeech.ts b/src/vs/workbench/api/browser/mainThreadSpeech.ts index fcb28dbc417..189a77fce2a 100644 --- a/src/vs/workbench/api/browser/mainThreadSpeech.ts +++ b/src/vs/workbench/api/browser/mainThreadSpeech.ts @@ -72,7 +72,7 @@ export class MainThreadSpeech implements MainThreadSpeechShape { onDidChange: onDidChange.event }; }, - createTextToSpeechSession: (token) => { + createTextToSpeechSession: (token, options) => { if (token.isCancellationRequested) { return { onDidChange: Event.None, @@ -83,7 +83,7 @@ export class MainThreadSpeech implements MainThreadSpeechShape { const disposables = new DisposableStore(); const session = Math.random(); - this.proxy.$createTextToSpeechSession(handle, session); + this.proxy.$createTextToSpeechSession(handle, session, options?.language); const onDidChange = disposables.add(new Emitter()); this.textToSpeechSessions.set(session, { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index c0babcbd2be..80f9d752b01 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1188,7 +1188,7 @@ export interface ExtHostSpeechShape { $createSpeechToTextSession(handle: number, session: number, language?: string): Promise; $cancelSpeechToTextSession(session: number): Promise; - $createTextToSpeechSession(handle: number, session: number): Promise; + $createTextToSpeechSession(handle: number, session: number, language?: string): Promise; $synthesizeSpeech(session: number, text: string): Promise; $cancelTextToSpeechSession(session: number): Promise; diff --git a/src/vs/workbench/api/common/extHostSpeech.ts b/src/vs/workbench/api/common/extHostSpeech.ts index da563dcdb4d..198eaee26ad 100644 --- a/src/vs/workbench/api/common/extHostSpeech.ts +++ b/src/vs/workbench/api/common/extHostSpeech.ts @@ -57,7 +57,7 @@ export class ExtHostSpeech implements ExtHostSpeechShape { this.sessions.delete(session); } - async $createTextToSpeechSession(handle: number, session: number): Promise { + async $createTextToSpeechSession(handle: number, session: number, language?: string): Promise { const provider = this.providers.get(handle); if (!provider) { return; @@ -68,7 +68,7 @@ export class ExtHostSpeech implements ExtHostSpeechShape { const cts = new CancellationTokenSource(); this.sessions.set(session, cts); - const textToSpeech = await provider.provideTextToSpeechSession(cts.token); + const textToSpeech = await provider.provideTextToSpeechSession(cts.token, language ? { language } : undefined); if (!textToSpeech) { return; } diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts index 381c77c96e8..dd0af503046 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts @@ -669,7 +669,7 @@ export class DynamicSpeechAccessibilityConfiguration extends Disposable implemen 'tags': ['accessibility'] }, [AccessibilityVoiceSettingId.SpeechLanguage]: { - 'markdownDescription': localize('voice.speechLanguage', "The language that voice speech recognition should recognize. Select `auto` to use the configured display language if possible. Note that not all display languages maybe supported by speech recognition"), + 'markdownDescription': localize('voice.speechLanguage', "The language that text-to-speech and speech-to-text should use. Select `auto` to use the configured display language if possible. Note that not all display languages maybe supported by speech recognition and synthesizers."), 'type': 'string', 'enum': languagesSorted, 'default': 'auto', diff --git a/src/vs/workbench/contrib/speech/browser/speechService.ts b/src/vs/workbench/contrib/speech/browser/speechService.ts index b0794137e25..7361470ddc8 100644 --- a/src/vs/workbench/contrib/speech/browser/speechService.ts +++ b/src/vs/workbench/contrib/speech/browser/speechService.ts @@ -256,7 +256,8 @@ export class SpeechService extends Disposable implements ISpeechService { async createTextToSpeechSession(token: CancellationToken, context: string = 'speech'): Promise { const provider = await this.getProvider(); - const session = this._activeTextToSpeechSession = provider.createTextToSpeechSession(token); + const language = speechLanguageConfigToLanguage(this.configurationService.getValue(SPEECH_LANGUAGE_CONFIG)); + const session = this._activeTextToSpeechSession = provider.createTextToSpeechSession(token, typeof language === 'string' ? { language } : undefined); const sessionStart = Date.now(); let sessionError = false; @@ -275,16 +276,19 @@ export class SpeechService extends Disposable implements ISpeechService { context: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Context of the session.' }; sessionDuration: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Duration of the session.' }; sessionError: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'If speech resulted in error.' }; + sessionLanguage: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Configured language for the session.' }; }; type TextToSpeechSessionEvent = { context: string; sessionDuration: number; sessionError: boolean; + sessionLanguage: string; }; this.telemetryService.publicLog2('textToSpeechSession', { context, sessionDuration: Date.now() - sessionStart, - sessionError + sessionError, + sessionLanguage: language }); } diff --git a/src/vs/workbench/contrib/speech/common/speechService.ts b/src/vs/workbench/contrib/speech/common/speechService.ts index 4bd76b641aa..4181ed15a63 100644 --- a/src/vs/workbench/contrib/speech/common/speechService.ts +++ b/src/vs/workbench/contrib/speech/common/speechService.ts @@ -76,11 +76,15 @@ export interface ISpeechToTextSessionOptions { readonly language?: string; } +export interface ITextToSpeechSessionOptions { + readonly language?: string; +} + export interface ISpeechProvider { readonly metadata: ISpeechProviderMetadata; createSpeechToTextSession(token: CancellationToken, options?: ISpeechToTextSessionOptions): ISpeechToTextSession; - createTextToSpeechSession(token: CancellationToken): ITextToSpeechSession; + createTextToSpeechSession(token: CancellationToken, options?: ITextToSpeechSessionOptions): ITextToSpeechSession; createKeywordRecognitionSession(token: CancellationToken): IKeywordRecognitionSession; } diff --git a/src/vscode-dts/vscode.proposed.speech.d.ts b/src/vscode-dts/vscode.proposed.speech.d.ts index b450ed18bf2..c78a32ddfd3 100644 --- a/src/vscode-dts/vscode.proposed.speech.d.ts +++ b/src/vscode-dts/vscode.proposed.speech.d.ts @@ -26,6 +26,10 @@ declare module 'vscode' { readonly onDidChange: Event; } + export interface TextToSpeechOptions { + readonly language?: string; + } + export enum TextToSpeechStatus { Started = 1, Stopped = 2, @@ -59,7 +63,7 @@ declare module 'vscode' { export interface SpeechProvider { provideSpeechToTextSession(token: CancellationToken, options?: SpeechToTextOptions): ProviderResult; - provideTextToSpeechSession(token: CancellationToken): ProviderResult; + provideTextToSpeechSession(token: CancellationToken, options?: TextToSpeechOptions): ProviderResult; provideKeywordRecognitionSession(token: CancellationToken): ProviderResult; } From 90dfd06ef12b71153e0ba108b1304c5c04deda5b Mon Sep 17 00:00:00 2001 From: Aaron Munger Date: Thu, 9 May 2024 09:12:26 -0700 Subject: [PATCH 069/357] contribute standard interactive execute keybindings from core (#212305) contribute shift+execute from core --- .../browser/interactive.contribution.ts | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts index 3a6737053eb..5c9e992b2d9 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts @@ -434,7 +434,21 @@ registerAction2(class extends Action2 { id: 'interactive.execute', title: localize2('interactive.execute', 'Execute Code'), category: interactiveWindowCategory, - keybinding: { + keybinding: [{ + when: ContextKeyExpr.and( + ContextKeyExpr.equals('activeEditor', 'workbench.editor.interactive'), + ContextKeyExpr.equals('config.interactiveWindow.executeWithShiftEnter', true) + ), + primary: KeyMod.Shift | KeyCode.Enter, + weight: NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT + }, { + when: ContextKeyExpr.and( + ContextKeyExpr.equals('activeEditor', 'workbench.editor.interactive'), + ContextKeyExpr.equals('config.interactiveWindow.executeWithShiftEnter', false) + ), + primary: KeyCode.Enter, + weight: NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT + }, { // when: NOTEBOOK_CELL_LIST_FOCUSED, when: ContextKeyExpr.equals('activeEditor', 'workbench.editor.interactive'), primary: KeyMod.WinCtrl | KeyCode.Enter, @@ -442,7 +456,7 @@ registerAction2(class extends Action2 { primary: KeyMod.CtrlCmd | KeyCode.Enter }, weight: NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT - }, + }], menu: [ { id: MenuId.InteractiveInputExecute @@ -810,3 +824,16 @@ Registry.as(ConfigurationExtensions.Configuration).regis } } }); + +Registry.as(ConfigurationExtensions.Configuration).registerConfiguration({ + id: 'interactiveWindow', + order: 100, + type: 'object', + 'properties': { + ['executeWithShiftEnter']: { + type: 'boolean', + default: true, + markdownDescription: localize('interactiveWindow.executeWithShiftEnter', "Execute the interactive window (REPL) input box with shift+enter, so that enter can be used to create a newline.") + } + } +}); From 997865bdec29e68a86b19994bcd34a4242a08020 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 9 May 2024 09:56:13 -0700 Subject: [PATCH 070/357] Show fully-qualified name for / completion as well --- .../browser/contrib/chatInputCompletions.ts | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts index 75f517745e1..8e036b5f016 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts @@ -116,19 +116,17 @@ class AgentCompletions extends Disposable { .filter(a => a.locations.includes(widget.location)); return { - suggestions: agents.map((a, i): CompletionItem => { - const isAllowed = this.chatAgentNameService.getAgentNameRestriction(a).get(); - const withAt = `${chatAgentLeader}${isAllowed ? a.name : getFullyQualifiedId(a)}`; - const isDupe = isAllowed && !!agents.find(other => other.name === a.name && other.id !== a.id); + suggestions: agents.map((agent, i): CompletionItem => { + const { label: agentLabel, isDupe } = getAgentCompletionDetails(agent, agents, this.chatAgentNameService); return { // Leading space is important because detail has no space at the start by design label: isDupe ? - { label: withAt, description: a.description, detail: ` (${a.publisherDisplayName})` } : - withAt, - insertText: `${withAt} `, - detail: a.description, + { label: agentLabel, description: agent.description, detail: ` (${agent.publisherDisplayName})` } : + agentLabel, + insertText: `${agentLabel} `, + detail: agent.description, range: new Range(1, 1, 1, 1), - command: { id: AssignSelectedAgentAction.ID, title: AssignSelectedAgentAction.ID, arguments: [{ agent: a, widget } satisfies AssignSelectedAgentActionArgs] }, + command: { id: AssignSelectedAgentAction.ID, title: AssignSelectedAgentAction.ID, arguments: [{ agent: agent, widget } satisfies AssignSelectedAgentActionArgs] }, kind: CompletionItemKind.Text, // The icons are disabled here anyway }; }) @@ -208,9 +206,8 @@ class AgentCompletions extends Disposable { const justAgents: CompletionItem[] = agents .filter(a => !a.isDefault) .map(agent => { - const isDupe = !!agents.find(other => other.name === agent.name && other.id !== agent.id); + const { label: agentLabel, isDupe } = getAgentCompletionDetails(agent, agents, this.chatAgentNameService); const detail = agent.description; - const agentLabel = `${chatAgentLeader}${agent.name}`; return { label: isDupe ? @@ -229,10 +226,10 @@ class AgentCompletions extends Disposable { return { suggestions: justAgents.concat( agents.flatMap(agent => agent.slashCommands.map((c, i) => { - const agentLabel = `${chatAgentLeader}${agent.name}`; + const { label: agentLabel, isDupe } = getAgentCompletionDetails(agent, agents, this.chatAgentNameService); const withSlash = `${chatSubcommandLeader}${c.name}`; return { - label: { label: withSlash, description: agentLabel }, + label: { label: withSlash, description: agentLabel, detail: isDupe ? ` (${agent.publisherDisplayName})` : undefined }, filterText: `${chatSubcommandLeader}${agent.name}${c.name}`, commitCharacters: [' '], insertText: `${agentLabel} ${withSlash} `, @@ -390,3 +387,11 @@ class VariableCompletions extends Disposable { } Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(VariableCompletions, LifecyclePhase.Eventually); + +function getAgentCompletionDetails(agent: IChatAgentData, otherAgents: IChatAgentData[], chatAgentNameService: IChatAgentNameService): { label: string; isDupe: boolean } { + const isAllowed = chatAgentNameService.getAgentNameRestriction(agent).get(); + const agentLabel = `${chatAgentLeader}${isAllowed ? agent.name : getFullyQualifiedId(agent)}`; + const isDupe = isAllowed && !!otherAgents.find(other => other.name === agent.name && other.id !== agent.id); + + return { label: agentLabel, isDupe }; +} From 268c20f317bd76b1b3162e5c13d9da3c743865d7 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Thu, 9 May 2024 10:20:21 -0700 Subject: [PATCH 071/357] Suppress semantic errors in js/ts notebook cells (#212367) Fixes #212366 --- .../typescript-language-features/src/languageProvider.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/extensions/typescript-language-features/src/languageProvider.ts b/extensions/typescript-language-features/src/languageProvider.ts index 6157c3b8cb4..6bd2b4ed7b7 100644 --- a/extensions/typescript-language-features/src/languageProvider.ts +++ b/extensions/typescript-language-features/src/languageProvider.ts @@ -9,6 +9,7 @@ import { CommandManager } from './commands/commandManager'; import { DocumentSelector } from './configuration/documentSelector'; import * as fileSchemes from './configuration/fileSchemes'; import { LanguageDescription } from './configuration/languageDescription'; +import { Schemes } from './configuration/schemes'; import { DiagnosticKind } from './languageFeatures/diagnostics'; import FileConfigurationManager from './languageFeatures/fileConfigurationManager'; import { TelemetryReporter } from './logging/telemetry'; @@ -145,6 +146,11 @@ export default class LanguageProvider extends Disposable { return; } + // Disable semantic errors in notebooks until we have better notebook support + if (diagnosticsKind === DiagnosticKind.Semantic && file.scheme === Schemes.notebookCell) { + return; + } + const config = vscode.workspace.getConfiguration(this.id, file); const reportUnnecessary = config.get('showUnused', true); const reportDeprecated = config.get('showDeprecated', true); From a4643b0ee0ae1bdf6a9a91b8ef64db5ba2d81bf2 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Thu, 9 May 2024 11:09:49 -0700 Subject: [PATCH 072/357] Enable web type acquisition by default for js/ts (#212370) * Enable web type acquisition by default for js/ts Fixes #182791 Fixes #172887 * Cleanup --- .../typescript-language-features/package.json | 15 +++++++------- .../package.nls.json | 4 ++-- .../src/configuration/configuration.ts | 18 ++++++++--------- .../src/extension.browser.ts | 20 ++++++++++++------- .../src/languageProvider.ts | 11 +++++++--- .../src/tsServer/serverProcess.browser.ts | 2 +- .../web/src/fileWatcherManager.ts | 2 -- 7 files changed, 40 insertions(+), 32 deletions(-) diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index fb5383a2978..45c687225b5 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -1280,22 +1280,21 @@ }, "typescript.tsserver.web.projectWideIntellisense.suppressSemanticErrors": { "type": "boolean", - "default": true, + "default": false, "description": "%configuration.tsserver.web.projectWideIntellisense.suppressSemanticErrors%", "scope": "window" }, + "typescript.tsserver.web.typeAcquisition.enabled": { + "type": "boolean", + "default": true, + "description": "%configuration.tsserver.web.typeAcquisition.enabled%", + "scope": "window" + }, "typescript.tsserver.nodePath": { "type": "string", "description": "%configuration.tsserver.nodePath%", "scope": "window" }, - "typescript.experimental.tsserver.web.typeAcquisition.enabled": { - "type": "boolean", - "default": false, - "description": "%configuration.experimental.tsserver.web.typeAcquisition.enabled%", - "scope": "window", - "tags": ["experimental"] - }, "typescript.preferGoToSourceDefinition": { "type": "boolean", "default": false, diff --git a/extensions/typescript-language-features/package.nls.json b/extensions/typescript-language-features/package.nls.json index e60451eaeb4..332f69b7140 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -216,9 +216,9 @@ "configuration.suggest.classMemberSnippets.enabled": "Enable/disable snippet completions for class members.", "configuration.suggest.objectLiteralMethodSnippets.enabled": "Enable/disable snippet completions for methods in object literals.", "configuration.tsserver.web.projectWideIntellisense.enabled": "Enable/disable project-wide IntelliSense on web. Requires that VS Code is running in a trusted context.", - "configuration.tsserver.web.projectWideIntellisense.suppressSemanticErrors": "Suppresses semantic errors. This is needed when using external packages as these can't be included analyzed on web.", + "configuration.tsserver.web.projectWideIntellisense.suppressSemanticErrors": "Suppresses semantic errors on web even when project wide IntelliSense is enabled. This is always on when project wide IntelliSense is not enabled or available. See `#typescript.tsserver.web.projectWideIntellisense.enabled#`", + "configuration.tsserver.web.typeAcquisition.enabled": "Enable/disable package acquisition on the web. This enables IntelliSense for imported packages. Requires `#typescript.tsserver.web.projectWideIntellisense.enabled#`.", "configuration.tsserver.nodePath": "Run TS Server on a custom Node installation. This can be a path to a Node executable, or 'node' if you want VS Code to detect a Node installation.", - "configuration.experimental.tsserver.web.typeAcquisition.enabled": "Enable/disable package acquisition on the web.", "walkthroughs.nodejsWelcome.title": "Get started with JavaScript and Node.js", "walkthroughs.nodejsWelcome.description": "Make the most of Visual Studio Code's first-class JavaScript experience.", "walkthroughs.nodejsWelcome.downloadNode.forMacOrWindows.title": "Install Node.js", diff --git a/extensions/typescript-language-features/src/configuration/configuration.ts b/extensions/typescript-language-features/src/configuration/configuration.ts index 47620bd0743..2f0ff4b0a28 100644 --- a/extensions/typescript-language-features/src/configuration/configuration.ts +++ b/extensions/typescript-language-features/src/configuration/configuration.ts @@ -112,7 +112,7 @@ export interface TypeScriptServiceConfiguration { readonly useSyntaxServer: SyntaxServerConfiguration; readonly webProjectWideIntellisenseEnabled: boolean; readonly webProjectWideIntellisenseSuppressSemanticErrors: boolean; - readonly webExperimentalTypeAcquisition: boolean; + readonly webTypeAcquisitionEnabled: boolean; readonly enableDiagnosticsTelemetry: boolean; readonly enableProjectDiagnostics: boolean; readonly maxTsServerMemory: number; @@ -150,7 +150,7 @@ export abstract class BaseServiceConfigurationProvider implements ServiceConfigu useSyntaxServer: this.readUseSyntaxServer(configuration), webProjectWideIntellisenseEnabled: this.readWebProjectWideIntellisenseEnable(configuration), webProjectWideIntellisenseSuppressSemanticErrors: this.readWebProjectWideIntellisenseSuppressSemanticErrors(configuration), - webExperimentalTypeAcquisition: this.readWebExperimentalTypeAcquisition(configuration), + webTypeAcquisitionEnabled: this.readWebTypeAcquisition(configuration), enableDiagnosticsTelemetry: this.readEnableDiagnosticsTelemetry(configuration), enableProjectDiagnostics: this.readEnableProjectDiagnostics(configuration), maxTsServerMemory: this.readMaxTsServerMemory(configuration), @@ -187,10 +187,6 @@ export abstract class BaseServiceConfigurationProvider implements ServiceConfigu return configuration.get('typescript.disableAutomaticTypeAcquisition', false); } - protected readWebExperimentalTypeAcquisition(configuration: vscode.WorkspaceConfiguration): boolean { - return configuration.get('typescript.experimental.tsserver.web.typeAcquisition.enabled', false); - } - protected readLocale(configuration: vscode.WorkspaceConfiguration): string | null { const value = configuration.get('typescript.locale', 'auto'); return !value || value === 'auto' ? null : value; @@ -256,15 +252,19 @@ export abstract class BaseServiceConfigurationProvider implements ServiceConfigu return configuration.get('typescript.tsserver.enableTracing', false); } + private readWorkspaceSymbolsExcludeLibrarySymbols(configuration: vscode.WorkspaceConfiguration): boolean { + return configuration.get('typescript.workspaceSymbols.excludeLibrarySymbols', true); + } + private readWebProjectWideIntellisenseEnable(configuration: vscode.WorkspaceConfiguration): boolean { return configuration.get('typescript.tsserver.web.projectWideIntellisense.enabled', true); } private readWebProjectWideIntellisenseSuppressSemanticErrors(configuration: vscode.WorkspaceConfiguration): boolean { - return configuration.get('typescript.tsserver.web.projectWideIntellisense.suppressSemanticErrors', true); + return configuration.get('typescript.tsserver.web.projectWideIntellisense.suppressSemanticErrors', false); } - private readWorkspaceSymbolsExcludeLibrarySymbols(configuration: vscode.WorkspaceConfiguration): boolean { - return configuration.get('typescript.workspaceSymbols.excludeLibrarySymbols', true); + private readWebTypeAcquisition(configuration: vscode.WorkspaceConfiguration): boolean { + return configuration.get('typescript.tsserver.web.typeAcquisition.enabled', true); } } diff --git a/extensions/typescript-language-features/src/extension.browser.ts b/extensions/typescript-language-features/src/extension.browser.ts index 91a652ed6fa..25a7669a326 100644 --- a/extensions/typescript-language-features/src/extension.browser.ts +++ b/extensions/typescript-language-features/src/extension.browser.ts @@ -62,7 +62,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { new TypeScriptVersion( TypeScriptVersionSource.Bundled, vscode.Uri.joinPath(context.extensionUri, 'dist/browser/typescript/tsserver.web.js').toString(), - API.fromSimpleString('5.3.2'))); + API.fromSimpleString('5.4.5'))); let experimentTelemetryReporter: IExperimentationTelemetryReporter | undefined; const packageInfo = getPackageInfo(context); @@ -118,15 +118,21 @@ async function startPreloadWorkspaceContentsIfNeeded(context: vscode.ExtensionCo return; } - const workspaceUri = vscode.workspace.workspaceFolders?.at(0)?.uri; - if (!workspaceUri || workspaceUri.scheme !== 'vscode-vfs' || !workspaceUri.authority.startsWith('github')) { - logger.info(`Skipped loading workspace contents for repository ${workspaceUri?.toString()}`); + if (!vscode.workspace.workspaceFolders) { return; } - const loader = new RemoteWorkspaceContentsPreloader(workspaceUri, logger); - context.subscriptions.push(loader); - return loader.triggerPreload(); + await Promise.all(vscode.workspace.workspaceFolders.map(async folder => { + const workspaceUri = folder.uri; + if (workspaceUri.scheme !== 'vscode-vfs' || !workspaceUri.authority.startsWith('github')) { + logger.info(`Skipped pre loading workspace contents for repository ${workspaceUri?.toString()}`); + return; + } + + const loader = new RemoteWorkspaceContentsPreloader(workspaceUri, logger); + context.subscriptions.push(loader); + await loader.triggerPreload(); + })); } class RemoteWorkspaceContentsPreloader extends Disposable { diff --git a/extensions/typescript-language-features/src/languageProvider.ts b/extensions/typescript-language-features/src/languageProvider.ts index 6bd2b4ed7b7..f44ebcc1212 100644 --- a/extensions/typescript-language-features/src/languageProvider.ts +++ b/extensions/typescript-language-features/src/languageProvider.ts @@ -18,7 +18,7 @@ import { ClientCapability } from './typescriptService'; import TypeScriptServiceClient from './typescriptServiceClient'; import TypingsStatus from './ui/typingsStatus'; import { Disposable } from './utils/dispose'; -import { isWeb } from './utils/platform'; +import { isWeb, isWebAndHasSharedArrayBuffers } from './utils/platform'; const validateSetting = 'validate.enable'; @@ -142,8 +142,13 @@ export default class LanguageProvider extends Disposable { return; } - if (diagnosticsKind === DiagnosticKind.Semantic && isWeb() && this.client.configuration.webProjectWideIntellisenseSuppressSemanticErrors) { - return; + if (diagnosticsKind === DiagnosticKind.Semantic && isWeb()) { + if (!isWebAndHasSharedArrayBuffers() + || this.client.configuration.webProjectWideIntellisenseSuppressSemanticErrors + || !this.client.configuration.webProjectWideIntellisenseEnabled + ) { + return; + } } // Disable semantic errors in notebooks until we have better notebook support diff --git a/extensions/typescript-language-features/src/tsServer/serverProcess.browser.ts b/extensions/typescript-language-features/src/tsServer/serverProcess.browser.ts index bb57c2644b4..8c5b8bfc527 100644 --- a/extensions/typescript-language-features/src/tsServer/serverProcess.browser.ts +++ b/extensions/typescript-language-features/src/tsServer/serverProcess.browser.ts @@ -50,7 +50,7 @@ export class WorkerServerProcessFactory implements TsServerProcessFactory { // Explicitly give TS Server its path so it can load local resources '--executingFilePath', tsServerPath, ]; - if (_configuration.webExperimentalTypeAcquisition) { + if (_configuration.webTypeAcquisitionEnabled) { launchArgs.push('--experimentalTypeAcquisition'); } return new WorkerServerProcess(kind, tsServerPath, this._extensionUri, launchArgs, tsServerLog, this._logger); diff --git a/extensions/typescript-language-features/web/src/fileWatcherManager.ts b/extensions/typescript-language-features/web/src/fileWatcherManager.ts index 8c8d7403740..5bbce244688 100644 --- a/extensions/typescript-language-features/web/src/fileWatcherManager.ts +++ b/extensions/typescript-language-features/web/src/fileWatcherManager.ts @@ -40,8 +40,6 @@ export class FileWatcherManager { return FileWatcherManager.noopWatcher; } - console.log('watching file:', path); - this.logger.logVerbose('fs.watchFile', { path }); let uri: URI; From 6f4fe62a51fc129cec46d9febfc86ae85769ea35 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Thu, 9 May 2024 09:41:29 -0700 Subject: [PATCH 073/357] fix: don't progressively render chat warnings --- .../workbench/contrib/chat/browser/chatListRenderer.ts | 9 ++++++++- src/vs/workbench/contrib/chat/common/chatViewModel.ts | 4 ++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 27e16b3f8b3..7182c105dea 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -70,7 +70,7 @@ import { ChatAgentLocation, IChatAgentMetadata, IChatAgentNameService } from 'vs import { CONTEXT_CHAT_RESPONSE_SUPPORT_ISSUE_REPORTING, CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_DETECTED_AGENT_COMMAND, CONTEXT_RESPONSE_FILTERED, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatProgressRenderableResponseContent, IChatTextEditGroup } from 'vs/workbench/contrib/chat/common/chatModel'; import { chatAgentLeader, chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { IChatCommandButton, IChatConfirmation, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseProgressFileTreeData, IChatSendRequestOptions, IChatService, IChatTask, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatCommandButton, IChatConfirmation, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseProgressFileTreeData, IChatSendRequestOptions, IChatService, IChatTask, IChatWarningMessage, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import { IChatProgressMessageRenderData, IChatRenderData, IChatResponseMarkdownRenderData, IChatResponseViewModel, IChatWelcomeMessageViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { IWordCountResult, getNWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; @@ -615,6 +615,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer Date: Thu, 9 May 2024 10:50:34 -0700 Subject: [PATCH 074/357] fix: rerender progress task when it settles --- .../api/common/extHostChatAgents2.ts | 13 ++++------ .../contrib/chat/browser/chatListRenderer.ts | 25 +++++++++++++------ .../contrib/chat/common/chatModel.ts | 16 ++++++------ .../contrib/chat/common/chatViewModel.ts | 7 +++++- 4 files changed, 36 insertions(+), 25 deletions(-) diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 885bb627bf0..1da6365884e 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -74,14 +74,11 @@ class ChatAgentResponseStream { this._firstProgress = this._stopWatch.elapsed(); } - this._proxy.$handleProgressChunk(this._request.requestId, progress) - .then((handle) => { - if (typeof handle === 'number' && task) { - task().then((res) => { - this._proxy.$handleProgressChunk(this._request.requestId, typeConvert.ChatTaskResult.from(res), handle); - }); - } - }); + Promise.all([this._proxy.$handleProgressChunk(this._request.requestId, progress,), task ? task() : undefined]).then(([handle, res]) => { + if (typeof handle === 'number' && task) { + this._proxy.$handleProgressChunk(this._request.requestId, typeConvert.ChatTaskResult.from(res), handle); + } + }); }; this._apiObject = { diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 7182c105dea..ca31e35b677 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -72,7 +72,7 @@ import { IChatProgressRenderableResponseContent, IChatTextEditGroup } from 'vs/w import { chatAgentLeader, chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatCommandButton, IChatConfirmation, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseProgressFileTreeData, IChatSendRequestOptions, IChatService, IChatTask, IChatWarningMessage, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; -import { IChatProgressMessageRenderData, IChatRenderData, IChatResponseMarkdownRenderData, IChatResponseViewModel, IChatWelcomeMessageViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; +import { IChatProgressMessageRenderData, IChatRenderData, IChatResponseMarkdownRenderData, IChatResponseViewModel, IChatTaskRenderData, IChatWelcomeMessageViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { IWordCountResult, getNWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; import { createFileIconThemableTreeContainerScope } from 'vs/workbench/contrib/files/browser/views/explorerView'; import { IFilesConfiguration } from 'vs/workbench/contrib/files/common/files'; @@ -615,9 +615,13 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { - // Replace the resolving part's content with the resolved response - if (typeof content === 'string') { - this._responseParts[responsePosition] = { ...progress, content: new MarkdownString(content) }; - } - this._updateRepr(false); - }); - } + progress.task?.().then((content) => { + // Replace the resolving part's content with the resolved response + if (typeof content === 'string') { + this._responseParts[responsePosition] = { ...progress, content: new MarkdownString(content) }; + } + this._updateRepr(false); + }); } else { this._responseParts.push(progress); this._updateRepr(quiet); diff --git a/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/chatViewModel.ts index f6e682804f7..3f34f8f239d 100644 --- a/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatViewModel.ts @@ -95,7 +95,12 @@ export interface IChatProgressMessageRenderData { isLast: boolean; } -export type IChatRenderData = IChatResponseProgressFileTreeData | IChatResponseMarkdownRenderData | IChatProgressMessageRenderData | IChatCommandButton | IChatTextEditGroup | IChatConfirmation | IChatTask | IChatWarningMessage; +export interface IChatTaskRenderData { + task: IChatTask; + isSettled: boolean; +} + +export type IChatRenderData = IChatResponseProgressFileTreeData | IChatResponseMarkdownRenderData | IChatProgressMessageRenderData | IChatCommandButton | IChatTextEditGroup | IChatConfirmation | IChatTaskRenderData | IChatWarningMessage; export interface IChatResponseRenderData { renderedParts: IChatRenderData[]; } From 12ad9eb3cf75ab970fe7339e5da02beab04e8e08 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Thu, 9 May 2024 10:56:20 -0700 Subject: [PATCH 075/357] refactor: clean up optional properties --- src/vs/workbench/api/common/extHost.protocol.ts | 3 ++- src/vs/workbench/api/common/extHostChatAgents2.ts | 8 ++++---- src/vs/workbench/api/common/extHostTypeConverters.ts | 4 ++-- src/vs/workbench/contrib/chat/common/chatService.ts | 4 ++-- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 80f9d752b01..190e7c4b45e 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1323,7 +1323,8 @@ export type IDocumentContextDto = { }; export type IChatProgressDto = - | Dto; + | Dto> + | IChatTaskDto; export interface ExtHostUrlsShape { $handleExternalUri(handle: number, uri: UriComponents): Promise; diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 1da6365884e..78a4076d962 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -17,12 +17,12 @@ import { URI } from 'vs/base/common/uri'; import { Location } from 'vs/editor/common/languages'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; -import { ExtHostChatAgentsShape2, IChatAgentCompletionItem, IChatAgentHistoryEntryDto, IExtensionChatAgentMetadata, IMainContext, MainContext, MainThreadChatAgentsShape2 } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostChatAgentsShape2, IChatAgentCompletionItem, IChatAgentHistoryEntryDto, IChatProgressDto, IExtensionChatAgentMetadata, IMainContext, MainContext, MainThreadChatAgentsShape2 } from 'vs/workbench/api/common/extHost.protocol'; import { CommandsConverter, ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; import { ChatAgentLocation, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { IChatContentReference, IChatFollowup, IChatProgress, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatContentReference, IChatFollowup, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { checkProposedApiEnabled, isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import { Dto } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import type * as vscode from 'vscode'; @@ -68,13 +68,13 @@ class ChatAgentResponseStream { } } - const _report = (progress: Dto, task?: () => Thenable) => { + const _report = (progress: IChatProgressDto, task?: () => Thenable) => { // Measure the time to the first progress update with real markdown content if (typeof this._firstProgress === 'undefined' && 'content' in progress) { this._firstProgress = this._stopWatch.elapsed(); } - Promise.all([this._proxy.$handleProgressChunk(this._request.requestId, progress,), task ? task() : undefined]).then(([handle, res]) => { + Promise.all([this._proxy.$handleProgressChunk(this._request.requestId, progress), task ? task() : undefined]).then(([handle, res]) => { if (typeof handle === 'number' && task) { this._proxy.$handleProgressChunk(this._request.requestId, typeConvert.ChatTaskResult.from(res), handle); } diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 3021b228fb4..b5dcd844713 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -40,7 +40,7 @@ import { DEFAULT_EDITOR_ASSOCIATION, SaveReason } from 'vs/workbench/common/edit import { IViewBadge } from 'vs/workbench/common/views'; import { ChatAgentLocation, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatRequestVariableEntry } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IChatAgentDetection, IChatAgentMarkdownContentWithVulnerability, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgressMessage, IChatTask, IChatTaskResult, IChatTextEdit, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatAgentDetection, IChatAgentMarkdownContentWithVulnerability, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgressMessage, IChatTaskDto, IChatTaskResult, IChatTextEdit, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from 'vs/workbench/contrib/chat/common/chatService'; import * as chatProvider from 'vs/workbench/contrib/chat/common/languageModels'; import { DebugTreeItemCollapsibleState, IDebugVisualizationTreeItem } from 'vs/workbench/contrib/debug/common/debug'; import * as notebooks from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -2392,7 +2392,7 @@ export namespace ChatResponseWarningPart { } export namespace ChatTask { - export function from(part: vscode.ChatResponseProgressPart2): Dto { + export function from(part: vscode.ChatResponseProgressPart2): IChatTaskDto { return { kind: 'progressTask', content: MarkdownString.from(part.value), diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 505dd820eda..3d60704721c 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -107,8 +107,8 @@ export interface IChatProgressMessage { } export interface IChatTask extends IChatTaskDto { - task?: () => Promise; - isSettled?: () => boolean; + task: () => Promise; + isSettled: () => boolean; } export interface IChatTaskDto { From 9518d13b2ff96380693e86dfcaf79294fa5b373c Mon Sep 17 00:00:00 2001 From: Aaron Munger Date: Thu, 9 May 2024 11:18:22 -0700 Subject: [PATCH 076/357] fix bad config registration (#212373) * fix bad config registration * fix bad config registration --- .../browser/interactive.contribution.ts | 20 ++----------------- 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts index 5c9e992b2d9..bc1ae32938e 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts @@ -808,28 +808,12 @@ Registry.as(ConfigurationExtensions.Configuration).regis type: 'boolean', default: true, markdownDescription: localize('interactiveWindow.alwaysScrollOnNewCell', "Automatically scroll the interactive window to show the output of the last statement executed. If this value is false, the window will only scroll if the last cell was already the one scrolled to.") - } - } -}); - -Registry.as(ConfigurationExtensions.Configuration).registerConfiguration({ - id: 'interactiveWindow', - order: 100, - type: 'object', - 'properties': { + }, [NotebookSetting.InteractiveWindowPromptToSave]: { type: 'boolean', default: false, markdownDescription: localize('interactiveWindow.promptToSaveOnClose', "Prompt to save the interactive window when it is closed. Only new interactive windows will be affected by this setting change.") - } - } -}); - -Registry.as(ConfigurationExtensions.Configuration).registerConfiguration({ - id: 'interactiveWindow', - order: 100, - type: 'object', - 'properties': { + }, ['executeWithShiftEnter']: { type: 'boolean', default: true, From 65f566e6d9f8aa6e19c986568a27197774fbb413 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 9 May 2024 11:20:31 -0700 Subject: [PATCH 077/357] Fix build --- src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts | 1 + 1 file changed, 1 insertion(+) 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 5e94b169a06..74ef88eca5f 100644 --- a/src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts @@ -66,6 +66,7 @@ suite('VoiceChat', () => { 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.'); } + getAgentByFullyQualifiedId(id: string): IChatAgentData | undefined { throw new Error('Method not implemented.'); } } class TestSpeechService implements ISpeechService { From 5447d0db107fd5af7fde9b05ddeafc4afdacb050 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 9 May 2024 11:31:51 -0700 Subject: [PATCH 078/357] bug fixes and such, format --- .../.vscode/launch.json | 2 +- .../.vscode/settings.json | 7 + .../package.json | 3 +- .../src/coverageProvider.ts | 91 ++--- .../src/extension.ts | 11 +- .../src/failingDeepStrictEqualAssertFixer.ts | 358 +++++++++--------- .../src/failureTracker.ts | 7 +- .../src/memoize.ts | 18 +- .../src/metadata.ts | 64 ++-- .../src/snapshot.ts | 22 +- .../src/sourceUtils.ts | 78 ++-- .../src/streamSplitter.ts | 6 +- .../src/testOutputScanner.ts | 14 +- .../src/testTree.ts | 240 ++++++------ .../src/v8CoverageWrangling.test.ts | 136 ++++--- .../src/v8CoverageWrangling.ts | 139 ++++--- .../src/vscodeTestRunner.ts | 6 +- package.json | 2 +- src/vs/workbench/api/common/extHostTesting.ts | 8 +- 19 files changed, 664 insertions(+), 548 deletions(-) create mode 100644 .vscode/extensions/vscode-selfhost-test-provider/.vscode/settings.json diff --git a/.vscode/extensions/vscode-selfhost-test-provider/.vscode/launch.json b/.vscode/extensions/vscode-selfhost-test-provider/.vscode/launch.json index fab178dfaeb..deb2c584c47 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/.vscode/launch.json +++ b/.vscode/extensions/vscode-selfhost-test-provider/.vscode/launch.json @@ -1,7 +1,7 @@ { "configurations": [ { - "args": ["--extensionDevelopmentPath=${workspaceFolder}"], + "args": ["--extensionDevelopmentPath=${workspaceFolder}", "--enable-proposed-api=ms-vscode.vscode-selfhost-test-provider"], "name": "Launch Extension", "outFiles": ["${workspaceFolder}/out/**/*.js"], "request": "launch", diff --git a/.vscode/extensions/vscode-selfhost-test-provider/.vscode/settings.json b/.vscode/extensions/vscode-selfhost-test-provider/.vscode/settings.json new file mode 100644 index 00000000000..e4429caeee4 --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "editor.formatOnSave": true, + "editor.defaultFormatter": "vscode.typescript-language-features", + "editor.codeActionsOnSave": { + "source.organizeImports": "always" + } +} diff --git a/.vscode/extensions/vscode-selfhost-test-provider/package.json b/.vscode/extensions/vscode-selfhost-test-provider/package.json index ffca4e554a0..852c6c29af2 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/package.json +++ b/.vscode/extensions/vscode-selfhost-test-provider/package.json @@ -3,7 +3,8 @@ "displayName": "VS Code Selfhost Test Provider", "description": "Test provider for the VS Code project", "enabledApiProposals": [ - "testObserver" + "testObserver", + "attributableCoverage" ], "engines": { "vscode": "^1.88.0" diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/coverageProvider.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/coverageProvider.ts index 8059a3d1e1f..7280782c10a 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/coverageProvider.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/coverageProvider.ts @@ -6,7 +6,7 @@ import { IstanbulCoverageContext } from 'istanbul-to-vscode'; import * as vscode from 'vscode'; import { SourceLocationMapper, SourceMapStore } from './testOutputScanner'; -import { ICoverageRange, IScriptCoverage, OffsetToPosition, RangeCoverageTracker } from './v8CoverageWrangling'; +import { IScriptCoverage, OffsetToPosition, RangeCoverageTracker } from './v8CoverageWrangling'; export const istanbulCoverageContext = new IstanbulCoverageContext(); @@ -18,7 +18,7 @@ export const istanbulCoverageContext = new IstanbulCoverageContext(); export class PerTestCoverageTracker { private readonly scripts = new Map(); - constructor(private readonly maps: SourceMapStore,) {} + constructor(private readonly maps: SourceMapStore) {} public add(coverage: IScriptCoverage, test?: vscode.TestItem) { const script = this.scripts.get(coverage.scriptId); @@ -54,7 +54,7 @@ class Script { constructor( public readonly uri: vscode.Uri, source: string, - private readonly maps: SourceMapStore, + private readonly maps: SourceMapStore ) { this.converter = new OffsetToPosition(source); } @@ -70,7 +70,7 @@ class Script { public async report(run: vscode.TestRun) { const mapper = await this.maps.getSourceLocationMapper(this.uri.toString()); - const originalUri = await this.maps.getSourceFile(this.uri.toString()) || this.uri; + const originalUri = (await this.maps.getSourceFile(this.uri.toString())) || this.uri; run.addCoverage(this.overall.report(originalUri, this.converter, mapper)); for (const [test, projection] of this.perItem) { @@ -80,28 +80,11 @@ class Script { } class ScriptCoverageTracker { - /** Range tracking for non-block coverage in the file */ - private file = new RangeCoverageTracker(); - /** Range tracking for block coverage in the file */ - private readonly blocks = new Map(); + private coverage = new RangeCoverageTracker(); public add(coverage: IScriptCoverage) { - for (const fn of coverage.functions) { - if (fn.isBlockCoverage) { - const key = `${fn.ranges[0].startOffset}/${fn.ranges[0].endOffset}`; - const block = this.blocks.get(key); - if (block) { - for (let i = 1; i < fn.ranges.length; i++) { - block.setCovered(fn.ranges[i].startOffset, fn.ranges[i].endOffset, fn.ranges[i].count > 0); - } - } else { - this.blocks.set(key, RangeCoverageTracker.initializeBlock(fn.ranges)); - } - } else { - for (const range of fn.ranges) { - this.file.setCovered(range.startOffset, range.endOffset, range.count > 0); - } - } + for (const range of RangeCoverageTracker.initializeBlocks(coverage.functions)) { + this.coverage.setCovered(range.start, range.end, range.covered); } } @@ -111,38 +94,44 @@ class ScriptCoverageTracker { * If a source location mapper is given, it assumes the `uri` is the mapped * URI, and that any unmapped locations/outside the URI should be ignored. */ - public report(uri: vscode.Uri, convert: OffsetToPosition, mapper: SourceLocationMapper | undefined, item?: vscode.TestItem): V8CoverageFile { - + public report( + uri: vscode.Uri, + convert: OffsetToPosition, + mapper: SourceLocationMapper | undefined, + item?: vscode.TestItem + ): V8CoverageFile { const file = new V8CoverageFile(uri, item); - async function handleBlock(range: ICoverageRange) { - const startLine = convert.getLineOfOffset(range.start); - const endLine = convert.getLineOfOffset(range.end); - for (let i = startLine; i <= endLine; i++) { - const start = new vscode.Position(i, i === startLine ? range.start - convert.lines[i] : 0); - const startMap = mapper?.(start.line, start.line); - const end = new vscode.Position(i, i === endLine ? range.end - convert.lines[i] : 0); - const endMap = startMap && mapper?.(end.line, end.line); - if (mapper && (!endMap || uri.toString().toLowerCase() !== endMap.uri.toString().toLowerCase())) { - return; - } - - const detail = new vscode.StatementCoverage(range.covered, startMap && endMap - ? new vscode.Range(startMap.range.start, endMap.range.end) - : new vscode.Range(start, end) - ); - - file.add(detail); + for (const range of this.coverage) { + if (range.start === range.end) { + continue; } - } - for (const range of this.file) { - handleBlock(range); - } + const startCov = convert.toLineColumn(range.start); + let start = new vscode.Position(startCov.line, startCov.column); - for (const block of this.blocks.values()) { - for (const range of block) { - handleBlock(range); + const endCov = convert.toLineColumn(range.end); + let end = new vscode.Position(endCov.line, endCov.column); + if (mapper) { + const startMap = mapper(start.line, start.character); + const endMap = startMap && mapper(end.line, end.character); + if (!endMap || uri.toString().toLowerCase() !== endMap.uri.toString().toLowerCase()) { + continue; + } + start = startMap.range.start; + end = endMap.range.end; + } + + for (let i = start.line; i <= end.line; i++) { + file.add( + new vscode.StatementCoverage( + range.covered, + new vscode.Range( + new vscode.Position(i, i === start.line ? start.character : 0), + new vscode.Position(i, i === end.line ? end.character : Number.MAX_SAFE_INTEGER) + ) + ) + ); } } diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts index c8b0d8f92f3..f68e7f99e89 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts @@ -25,7 +25,7 @@ const TEST_FILE_PATTERN = 'src/vs/**/*.{test,integrationTest}.ts'; const getWorkspaceFolderForTestFile = (uri: vscode.Uri) => (uri.path.endsWith('.test.ts') || uri.path.endsWith('.integrationTest.ts')) && - uri.path.includes('/src/vs/') + uri.path.includes('/src/vs/') ? vscode.workspace.getWorkspaceFolder(uri) : undefined; @@ -58,7 +58,7 @@ export async function activate(context: vscode.ExtensionContext) { let startedTrackingFailures = false; const createRunHandler = ( - runnerCtor: { new(folder: vscode.WorkspaceFolder): VSCodeTestRunner }, + runnerCtor: { new (folder: vscode.WorkspaceFolder): VSCodeTestRunner }, kind: vscode.TestRunProfileKind, args: string[] = [] ) => { @@ -88,7 +88,10 @@ export async function activate(context: vscode.ExtensionContext) { if (kind === vscode.TestRunProfileKind.Coverage) { // todo: browser runs currently don't support per-test coverage if (args.includes('--browser')) { - coverageDir = path.join(tmpdir(), `vscode-test-coverage-${randomBytes(8).toString('hex')}`); + coverageDir = path.join( + tmpdir(), + `vscode-test-coverage-${randomBytes(8).toString('hex')}` + ); currentArgs = [ ...currentArgs, '--coverage', @@ -242,7 +245,7 @@ export async function activate(context: vscode.ExtensionContext) { vscode.workspace.onDidOpenTextDocument(updateNodeForDocument), vscode.workspace.onDidChangeTextDocument(e => updateNodeForDocument(e.document)), registerSnapshotUpdate(ctrl), - new FailingDeepStrictEqualAssertFixer(), + new FailingDeepStrictEqualAssertFixer() ); } diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/failingDeepStrictEqualAssertFixer.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/failingDeepStrictEqualAssertFixer.ts index b0494471f29..bd2a35d7abf 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/failingDeepStrictEqualAssertFixer.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/failingDeepStrictEqualAssertFixer.ts @@ -5,81 +5,81 @@ import * as ts from 'typescript'; import { - commands, - Disposable, - languages, - Position, - Range, - TestMessage, - TestResultSnapshot, - TestRunResult, - tests, - TextDocument, - Uri, - workspace, - WorkspaceEdit, + commands, + Disposable, + languages, + Position, + Range, + TestMessage, + TestResultSnapshot, + TestRunResult, + tests, + TextDocument, + Uri, + workspace, + WorkspaceEdit, } from 'vscode'; import { memoizeLast } from './memoize'; import { getTestMessageMetadata } from './metadata'; const enum Constants { - FixCommandId = 'selfhost-test.fix-test', + FixCommandId = 'selfhost-test.fix-test', } export class FailingDeepStrictEqualAssertFixer { - private disposables: Disposable[] = []; + private disposables: Disposable[] = []; - constructor() { - this.disposables.push( - commands.registerCommand(Constants.FixCommandId, async (uri: Uri, position: Position) => { - const document = await workspace.openTextDocument(uri); + constructor() { + this.disposables.push( + commands.registerCommand(Constants.FixCommandId, async (uri: Uri, position: Position) => { + const document = await workspace.openTextDocument(uri); - const failingAssertion = detectFailingDeepStrictEqualAssertion(document, position); - if (!failingAssertion) { - return; - } + const failingAssertion = detectFailingDeepStrictEqualAssertion(document, position); + if (!failingAssertion) { + return; + } - const expectedValueNode = failingAssertion.assertion.expectedValue; - if (!expectedValueNode) { - return; - } + const expectedValueNode = failingAssertion.assertion.expectedValue; + if (!expectedValueNode) { + return; + } - const start = document.positionAt(expectedValueNode.getStart()); - const end = document.positionAt(expectedValueNode.getEnd()); + const start = document.positionAt(expectedValueNode.getStart()); + const end = document.positionAt(expectedValueNode.getEnd()); - const edit = new WorkspaceEdit(); - edit.replace(uri, new Range(start, end), formatJsonValue(failingAssertion.actualJSONValue)); - await workspace.applyEdit(edit); - }) - ); + const edit = new WorkspaceEdit(); + edit.replace(uri, new Range(start, end), formatJsonValue(failingAssertion.actualJSONValue)); + await workspace.applyEdit(edit); + }) + ); - this.disposables.push( - languages.registerCodeActionsProvider('typescript', { - provideCodeActions: (document, range) => { - const failingAssertion = detectFailingDeepStrictEqualAssertion(document, range.start); - if (!failingAssertion) { - return undefined; - } + this.disposables.push( + languages.registerCodeActionsProvider('typescript', { + provideCodeActions: (document, range) => { + const failingAssertion = detectFailingDeepStrictEqualAssertion(document, range.start); + if (!failingAssertion) { + return undefined; + } - return [ - { - title: 'Fix Expected Value', - command: Constants.FixCommandId, - arguments: [document.uri, range.start], - }, - ]; - }, - }) - ); + return [ + { + title: 'Fix Expected Value', + command: Constants.FixCommandId, + arguments: [document.uri, range.start], + }, + ]; + }, + }) + ); - tests.testResults; - } + tests.testResults; + } - dispose() { - for (const d of this.disposables) { - d.dispose(); - } - } + dispose() { + for (const d of this.disposables) { + d.dispose(); + } + } } const identifierLikeRe = /^[$a-z_][a-z0-9_$]*$/i; @@ -87,170 +87,170 @@ const identifierLikeRe = /^[$a-z_][a-z0-9_$]*$/i; const tsPrinter = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); const formatJsonValue = (value: unknown) => { - if (typeof value !== 'object') { - return JSON.stringify(value); - } + if (typeof value !== 'object') { + return JSON.stringify(value); + } - const src = ts.createSourceFile('', `(${JSON.stringify(value)})`, ts.ScriptTarget.ES5, true); - const outerExpression = src.statements[0] as ts.ExpressionStatement; - const parenExpression = outerExpression.expression as ts.ParenthesizedExpression; + const src = ts.createSourceFile('', `(${JSON.stringify(value)})`, ts.ScriptTarget.ES5, true); + const outerExpression = src.statements[0] as ts.ExpressionStatement; + const parenExpression = outerExpression.expression as ts.ParenthesizedExpression; - const unquoted = ts.transform(parenExpression, [ - context => (node: ts.Node) => { - const visitor = (node: ts.Node): ts.Node => - ts.isPropertyAssignment(node) && - ts.isStringLiteralLike(node.name) && - identifierLikeRe.test(node.name.text) - ? ts.factory.createPropertyAssignment( - ts.factory.createIdentifier(node.name.text), - ts.visitNode(node.initializer, visitor) as ts.Expression - ) - : ts.isStringLiteralLike(node) && node.text === '[undefined]' - ? ts.factory.createIdentifier('undefined') - : ts.visitEachChild(node, visitor, context); + const unquoted = ts.transform(parenExpression, [ + context => (node: ts.Node) => { + const visitor = (node: ts.Node): ts.Node => + ts.isPropertyAssignment(node) && + ts.isStringLiteralLike(node.name) && + identifierLikeRe.test(node.name.text) + ? ts.factory.createPropertyAssignment( + ts.factory.createIdentifier(node.name.text), + ts.visitNode(node.initializer, visitor) as ts.Expression + ) + : ts.isStringLiteralLike(node) && node.text === '[undefined]' + ? ts.factory.createIdentifier('undefined') + : ts.visitEachChild(node, visitor, context); - return ts.visitNode(node, visitor); - }, - ]); + return ts.visitNode(node, visitor); + }, + ]); - return tsPrinter.printNode(ts.EmitHint.Expression, unquoted.transformed[0], src); + return tsPrinter.printNode(ts.EmitHint.Expression, unquoted.transformed[0], src); }; /** Parses the source file, memoizing the last document so cursor moves are efficient */ const parseSourceFile = memoizeLast((text: string) => - ts.createSourceFile('', text, ts.ScriptTarget.ES5, true) + ts.createSourceFile('', text, ts.ScriptTarget.ES5, true) ); const assertionFailureMessageRe = /^Expected values to be strictly (deep-)?equal:/; /** Gets information about the failing assertion at the poisition, if any. */ function detectFailingDeepStrictEqualAssertion( - document: TextDocument, - position: Position + document: TextDocument, + position: Position ): { assertion: StrictEqualAssertion; actualJSONValue: unknown } | undefined { - const sf = parseSourceFile(document.getText()); - const offset = document.offsetAt(position); - const assertion = StrictEqualAssertion.atPosition(sf, offset); - if (!assertion) { - return undefined; - } + const sf = parseSourceFile(document.getText()); + const offset = document.offsetAt(position); + const assertion = StrictEqualAssertion.atPosition(sf, offset); + if (!assertion) { + return undefined; + } - const startLine = document.positionAt(assertion.offsetStart).line; - const messages = getAllTestStatusMessagesAt(document.uri, startLine); - const strictDeepEqualMessage = messages.find(m => - assertionFailureMessageRe.test(typeof m.message === 'string' ? m.message : m.message.value) - ); + const startLine = document.positionAt(assertion.offsetStart).line; + const messages = getAllTestStatusMessagesAt(document.uri, startLine); + const strictDeepEqualMessage = messages.find(m => + assertionFailureMessageRe.test(typeof m.message === 'string' ? m.message : m.message.value) + ); - if (!strictDeepEqualMessage) { - return undefined; - } + if (!strictDeepEqualMessage) { + return undefined; + } - const metadata = getTestMessageMetadata(strictDeepEqualMessage); - if (!metadata) { - return undefined; - } + const metadata = getTestMessageMetadata(strictDeepEqualMessage); + if (!metadata) { + return undefined; + } - return { - assertion: assertion, - actualJSONValue: metadata.actualValue, - }; + return { + assertion: assertion, + actualJSONValue: metadata.actualValue, + }; } class StrictEqualAssertion { - /** - * Extracts the assertion at the current node, if it is one. - */ - public static fromNode(node: ts.Node): StrictEqualAssertion | undefined { - if (!ts.isCallExpression(node)) { - return undefined; - } + /** + * Extracts the assertion at the current node, if it is one. + */ + public static fromNode(node: ts.Node): StrictEqualAssertion | undefined { + if (!ts.isCallExpression(node)) { + return undefined; + } - const expr = node.expression.getText(); - if (expr !== 'assert.deepStrictEqual' && expr !== 'assert.strictEqual') { - return undefined; - } + const expr = node.expression.getText(); + if (expr !== 'assert.deepStrictEqual' && expr !== 'assert.strictEqual') { + return undefined; + } - return new StrictEqualAssertion(node); - } + return new StrictEqualAssertion(node); + } - /** - * Gets the equals assertion at the given offset in the file. - */ - public static atPosition(sf: ts.SourceFile, offset: number): StrictEqualAssertion | undefined { - let node = findNodeAt(sf, offset); + /** + * Gets the equals assertion at the given offset in the file. + */ + public static atPosition(sf: ts.SourceFile, offset: number): StrictEqualAssertion | undefined { + let node = findNodeAt(sf, offset); - while (node.parent) { - const obj = StrictEqualAssertion.fromNode(node); - if (obj) { - return obj; - } - node = node.parent; - } + while (node.parent) { + const obj = StrictEqualAssertion.fromNode(node); + if (obj) { + return obj; + } + node = node.parent; + } - return undefined; - } + return undefined; + } - constructor(private readonly expression: ts.CallExpression) { } + constructor(private readonly expression: ts.CallExpression) {} - /** Gets the expected value */ - public get expectedValue(): ts.Expression | undefined { - return this.expression.arguments[1]; - } + /** Gets the expected value */ + public get expectedValue(): ts.Expression | undefined { + return this.expression.arguments[1]; + } - /** Gets the position of the assertion expression. */ - public get offsetStart(): number { - return this.expression.getStart(); - } + /** Gets the position of the assertion expression. */ + public get offsetStart(): number { + return this.expression.getStart(); + } } function findNodeAt(parent: ts.Node, offset: number): ts.Node { - for (const child of parent.getChildren()) { - if (child.getStart() <= offset && offset <= child.getEnd()) { - return findNodeAt(child, offset); - } - } - return parent; + for (const child of parent.getChildren()) { + if (child.getStart() <= offset && offset <= child.getEnd()) { + return findNodeAt(child, offset); + } + } + return parent; } function getAllTestStatusMessagesAt(uri: Uri, lineNumber: number): TestMessage[] { - if (tests.testResults.length === 0) { - return []; - } + if (tests.testResults.length === 0) { + return []; + } - const run = tests.testResults[0]; - const snapshots = getTestResultsWithUri(run, uri); - const result: TestMessage[] = []; + const run = tests.testResults[0]; + const snapshots = getTestResultsWithUri(run, uri); + const result: TestMessage[] = []; - for (const snapshot of snapshots) { - for (const m of snapshot.taskStates[0].messages) { - if ( - m.location && - m.location.range.start.line <= lineNumber && - lineNumber <= m.location.range.end.line - ) { - result.push(m); - } - } - } + for (const snapshot of snapshots) { + for (const m of snapshot.taskStates[0].messages) { + if ( + m.location && + m.location.range.start.line <= lineNumber && + lineNumber <= m.location.range.end.line + ) { + result.push(m); + } + } + } - return result; + return result; } function getTestResultsWithUri(testRun: TestRunResult, uri: Uri): TestResultSnapshot[] { - const results: TestResultSnapshot[] = []; + const results: TestResultSnapshot[] = []; - const walk = (r: TestResultSnapshot) => { - for (const c of r.children) { - walk(c); - } - if (r.uri?.toString() === uri.toString()) { - results.push(r); - } - }; + const walk = (r: TestResultSnapshot) => { + for (const c of r.children) { + walk(c); + } + if (r.uri?.toString() === uri.toString()) { + results.push(r); + } + }; - for (const r of testRun.results) { - walk(r); - } + for (const r of testRun.results) { + walk(r); + } - return results; + return results; } diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/failureTracker.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/failureTracker.ts index a3e4c08530d..e232fa133e3 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/failureTracker.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/failureTracker.ts @@ -56,7 +56,12 @@ export class FailureTracker { const prev = this.lastFailed.get(key); if (snapshot.taskStates.some(s => s.state === vscode.TestResultState.Failed)) { // unset the parent to avoid a circular JSON structure: - getGitState().then(s => this.lastFailed.set(key, { snapshot: { ...snapshot, parent: undefined }, failing: s })); + getGitState().then(s => + this.lastFailed.set(key, { + snapshot: { ...snapshot, parent: undefined }, + failing: s, + }) + ); } else if (prev) { this.lastFailed.delete(key); getGitState().then(s => this.append({ ...prev, passing: s })); diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/memoize.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/memoize.ts index 52c2aa2c98f..df6c2e77ed2 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/memoize.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/memoize.ts @@ -4,14 +4,14 @@ *--------------------------------------------------------------------------------------------*/ export const memoizeLast = (fn: (args: A) => T): ((args: A) => T) => { - let last: { arg: A; result: T } | undefined; - return arg => { - if (last && last.arg === arg) { - return last.result; - } + let last: { arg: A; result: T } | undefined; + return arg => { + if (last && last.arg === arg) { + return last.result; + } - const result = fn(arg); - last = { arg, result }; - return result; - }; + const result = fn(arg); + last = { arg, result }; + return result; + }; }; diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/metadata.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/metadata.ts index 08540b14fbf..8b44c52b72f 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/metadata.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/metadata.ts @@ -5,8 +5,8 @@ import { TestMessage } from 'vscode'; export interface TestMessageMetadata { - expectedValue: unknown; - actualValue: unknown; + expectedValue: unknown; + actualValue: unknown; } const cache = new Array<{ id: string; metadata: TestMessageMetadata }>(); @@ -14,48 +14,48 @@ const cache = new Array<{ id: string; metadata: TestMessageMetadata }>(); let id = 0; function getId(): string { - return `msg:${id++}:`; + return `msg:${id++}:`; } const regexp = /msg:\d+:/; export function attachTestMessageMetadata( - message: TestMessage, - metadata: TestMessageMetadata + message: TestMessage, + metadata: TestMessageMetadata ): void { - const existingMetadata = getTestMessageMetadata(message); - if (existingMetadata) { - Object.assign(existingMetadata, metadata); - return; - } + const existingMetadata = getTestMessageMetadata(message); + if (existingMetadata) { + Object.assign(existingMetadata, metadata); + return; + } - const id = getId(); + const id = getId(); - if (typeof message.message === 'string') { - message.message = `${message.message}\n${id}`; - } else { - message.message.appendText(`\n${id}`); - } + if (typeof message.message === 'string') { + message.message = `${message.message}\n${id}`; + } else { + message.message.appendText(`\n${id}`); + } - cache.push({ id, metadata }); - while (cache.length > 100) { - cache.shift(); - } + cache.push({ id, metadata }); + while (cache.length > 100) { + cache.shift(); + } } export function getTestMessageMetadata(message: TestMessage): TestMessageMetadata | undefined { - let value: string; - if (typeof message.message === 'string') { - value = message.message; - } else { - value = message.message.value; - } + let value: string; + if (typeof message.message === 'string') { + value = message.message; + } else { + value = message.message.value; + } - const result = regexp.exec(value); - if (!result) { - return undefined; - } + const result = regexp.exec(value); + if (!result) { + return undefined; + } - const id = result[0]; - return cache.find(c => c.id === id)?.metadata; + const id = result[0]; + return cache.find(c => c.id === id)?.metadata; } diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/snapshot.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/snapshot.ts index 33fbc8fa8bb..5b3a624617e 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/snapshot.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/snapshot.ts @@ -9,15 +9,15 @@ import * as vscode from 'vscode'; export const snapshotComment = '\n\n// Snapshot file: '; export const registerSnapshotUpdate = (ctrl: vscode.TestController) => - vscode.commands.registerCommand('selfhost-test-provider.updateSnapshot', async args => { - const message: vscode.TestMessage = args.message; - const index = message.expectedOutput?.indexOf(snapshotComment); - if (!message.expectedOutput || !message.actualOutput || !index || index === -1) { - vscode.window.showErrorMessage('Could not find snapshot comment in message'); - return; - } + vscode.commands.registerCommand('selfhost-test-provider.updateSnapshot', async args => { + const message: vscode.TestMessage = args.message; + const index = message.expectedOutput?.indexOf(snapshotComment); + if (!message.expectedOutput || !message.actualOutput || !index || index === -1) { + vscode.window.showErrorMessage('Could not find snapshot comment in message'); + return; + } - const file = message.expectedOutput.slice(index + snapshotComment.length); - await fs.writeFile(file, message.actualOutput); - ctrl.invalidateTestResults(args.test); - }); + const file = message.expectedOutput.slice(index + snapshotComment.length); + await fs.writeFile(file, message.actualOutput); + ctrl.invalidateTestResults(args.test); + }); diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/sourceUtils.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/sourceUtils.ts index 944d3bc6f6d..56b26cafda8 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/sourceUtils.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/sourceUtils.ts @@ -10,59 +10,59 @@ import { TestCase, TestConstruct, TestSuite, VSCodeTest } from './testTree'; const suiteNames = new Set(['suite', 'flakySuite']); export const enum Action { - Skip, - Recurse, + Skip, + Recurse, } export const extractTestFromNode = (src: ts.SourceFile, node: ts.Node, parent: VSCodeTest) => { - if (!ts.isCallExpression(node)) { - return Action.Recurse; - } + if (!ts.isCallExpression(node)) { + return Action.Recurse; + } - let lhs = node.expression; - if (isSkipCall(lhs)) { - return Action.Skip; - } + let lhs = node.expression; + if (isSkipCall(lhs)) { + return Action.Skip; + } - if (isPropertyCall(lhs) && lhs.name.text === 'only') { - lhs = lhs.expression; - } + if (isPropertyCall(lhs) && lhs.name.text === 'only') { + lhs = lhs.expression; + } - const name = node.arguments[0]; - const func = node.arguments[1]; - if (!name || !ts.isIdentifier(lhs) || !ts.isStringLiteralLike(name)) { - return Action.Recurse; - } + const name = node.arguments[0]; + const func = node.arguments[1]; + if (!name || !ts.isIdentifier(lhs) || !ts.isStringLiteralLike(name)) { + return Action.Recurse; + } - if (!func) { - return Action.Recurse; - } + if (!func) { + return Action.Recurse; + } - const start = src.getLineAndCharacterOfPosition(name.pos); - const end = src.getLineAndCharacterOfPosition(func.end); - const range = new vscode.Range( - new vscode.Position(start.line, start.character), - new vscode.Position(end.line, end.character) - ); + const start = src.getLineAndCharacterOfPosition(name.pos); + const end = src.getLineAndCharacterOfPosition(func.end); + const range = new vscode.Range( + new vscode.Position(start.line, start.character), + new vscode.Position(end.line, end.character) + ); - const cparent = parent instanceof TestConstruct ? parent : undefined; - if (lhs.escapedText === 'test') { - return new TestCase(name.text, range, cparent); - } + const cparent = parent instanceof TestConstruct ? parent : undefined; + if (lhs.escapedText === 'test') { + return new TestCase(name.text, range, cparent); + } - if (suiteNames.has(lhs.escapedText.toString())) { - return new TestSuite(name.text, range, cparent); - } + if (suiteNames.has(lhs.escapedText.toString())) { + return new TestSuite(name.text, range, cparent); + } - return Action.Recurse; + return Action.Recurse; }; const isPropertyCall = ( - lhs: ts.LeftHandSideExpression + lhs: ts.LeftHandSideExpression ): lhs is ts.PropertyAccessExpression & { expression: ts.Identifier; name: ts.Identifier } => - ts.isPropertyAccessExpression(lhs) && - ts.isIdentifier(lhs.expression) && - ts.isIdentifier(lhs.name); + ts.isPropertyAccessExpression(lhs) && + ts.isIdentifier(lhs.expression) && + ts.isIdentifier(lhs.name); const isSkipCall = (lhs: ts.LeftHandSideExpression) => - isPropertyCall(lhs) && suiteNames.has(lhs.expression.text) && lhs.name.text === 'skip'; + isPropertyCall(lhs) && suiteNames.has(lhs.expression.text) && lhs.name.text === 'skip'; diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/streamSplitter.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/streamSplitter.ts index fd28b3772da..bb5bc5f28dd 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/streamSplitter.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/streamSplitter.ts @@ -28,7 +28,11 @@ export class StreamSplitter extends Transform { } } - override _transform(chunk: Buffer, _encoding: string, callback: (error?: Error | null, data?: any) => void): void { + override _transform( + chunk: Buffer, + _encoding: string, + callback: (error?: Error | null, data?: any) => void + ): void { if (!this.buffer) { this.buffer = chunk; } else { diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts index fceb5c3549d..1a1c21fd2c8 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { + decodedMappings, GREATEST_LOWER_BOUND, LEAST_UPPER_BOUND, originalPositionFor, @@ -15,8 +16,8 @@ import * as vscode from 'vscode'; import { istanbulCoverageContext, PerTestCoverageTracker } from './coverageProvider'; import { attachTestMessageMetadata } from './metadata'; import { snapshotComment } from './snapshot'; -import { getContentFromFilesystem } from './testTree'; import { StreamSplitter } from './streamSplitter'; +import { getContentFromFilesystem } from './testTree'; import { IScriptCoverage } from './v8CoverageWrangling'; export const enum MochaEvent { @@ -435,8 +436,17 @@ export class SourceMapStore { return undefined; } + let smLine = line + 1; + + // if the range is after the end of mappings, adjust it to the last mapped line + const decoded = decodedMappings(sourceMap); + if (decoded.length <= line) { + smLine = decoded.length; // base 1, no -1 needed + col = Number.MAX_SAFE_INTEGER; + } + for (const bias of sourceMapBiases) { - const position = originalPositionFor(sourceMap, { column: col, line: line + 1, bias }); + const position = originalPositionFor(sourceMap, { column: col, line: smLine, bias }); if (position.line !== null && position.column !== null && position.source !== null) { return new vscode.Location( this.completeSourceMapUrl(sourceMap, position.source), diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/testTree.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/testTree.ts index 6ecfb8bc07f..7a54c5c0d32 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/testTree.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/testTree.ts @@ -22,155 +22,155 @@ export const clearFileDiagnostics = (uri: vscode.Uri) => diagnosticCollection.de * Tries to guess which workspace folder VS Code is in. */ export const guessWorkspaceFolder = async () => { - if (!vscode.workspace.workspaceFolders) { - return undefined; - } + if (!vscode.workspace.workspaceFolders) { + return undefined; + } - if (vscode.workspace.workspaceFolders.length < 2) { - return vscode.workspace.workspaceFolders[0]; - } + if (vscode.workspace.workspaceFolders.length < 2) { + return vscode.workspace.workspaceFolders[0]; + } - for (const folder of vscode.workspace.workspaceFolders) { - try { - await vscode.workspace.fs.stat(vscode.Uri.joinPath(folder.uri, 'src/vs/loader.js')); - return folder; - } catch { - // ignored - } - } + for (const folder of vscode.workspace.workspaceFolders) { + try { + await vscode.workspace.fs.stat(vscode.Uri.joinPath(folder.uri, 'src/vs/loader.js')); + return folder; + } catch { + // ignored + } + } - return undefined; + return undefined; }; export const getContentFromFilesystem: ContentGetter = async uri => { - try { - const rawContent = await vscode.workspace.fs.readFile(uri); - return textDecoder.decode(rawContent); - } catch (e) { - console.warn(`Error providing tests for ${uri.fsPath}`, e); - return ''; - } + try { + const rawContent = await vscode.workspace.fs.readFile(uri); + return textDecoder.decode(rawContent); + } catch (e) { + console.warn(`Error providing tests for ${uri.fsPath}`, e); + return ''; + } }; export class TestFile { - public hasBeenRead = false; + public hasBeenRead = false; - constructor( - public readonly uri: vscode.Uri, - public readonly workspaceFolder: vscode.WorkspaceFolder - ) { } + constructor( + public readonly uri: vscode.Uri, + public readonly workspaceFolder: vscode.WorkspaceFolder + ) {} - public getId() { - return this.uri.toString().toLowerCase(); - } + public getId() { + return this.uri.toString().toLowerCase(); + } - public getLabel() { - return relative(join(this.workspaceFolder.uri.fsPath, 'src'), this.uri.fsPath); - } + public getLabel() { + return relative(join(this.workspaceFolder.uri.fsPath, 'src'), this.uri.fsPath); + } - public async updateFromDisk(controller: vscode.TestController, item: vscode.TestItem) { - try { - const content = await getContentFromFilesystem(item.uri!); - item.error = undefined; - this.updateFromContents(controller, content, item); - } catch (e) { - item.error = (e as Error).stack; - } - } + public async updateFromDisk(controller: vscode.TestController, item: vscode.TestItem) { + try { + const content = await getContentFromFilesystem(item.uri!); + item.error = undefined; + this.updateFromContents(controller, content, item); + } catch (e) { + item.error = (e as Error).stack; + } + } - /** - * Refreshes all tests in this file, `sourceReader` provided by the root. - */ - public updateFromContents( - controller: vscode.TestController, - content: string, - file: vscode.TestItem - ) { - try { - const diagnostics: vscode.Diagnostic[] = []; - const ast = ts.createSourceFile( - this.uri.path.split('/').pop()!, - content, - ts.ScriptTarget.ESNext, - false, - ts.ScriptKind.TS - ); + /** + * Refreshes all tests in this file, `sourceReader` provided by the root. + */ + public updateFromContents( + controller: vscode.TestController, + content: string, + file: vscode.TestItem + ) { + try { + const diagnostics: vscode.Diagnostic[] = []; + const ast = ts.createSourceFile( + this.uri.path.split('/').pop()!, + content, + ts.ScriptTarget.ESNext, + false, + ts.ScriptKind.TS + ); - const parents: { item: vscode.TestItem; children: vscode.TestItem[] }[] = [ - { item: file, children: [] }, - ]; - const traverse = (node: ts.Node) => { - const parent = parents[parents.length - 1]; - const childData = extractTestFromNode(ast, node, itemData.get(parent.item)!); - if (childData === Action.Skip) { - return; - } + const parents: { item: vscode.TestItem; children: vscode.TestItem[] }[] = [ + { item: file, children: [] }, + ]; + const traverse = (node: ts.Node) => { + const parent = parents[parents.length - 1]; + const childData = extractTestFromNode(ast, node, itemData.get(parent.item)!); + if (childData === Action.Skip) { + return; + } - if (childData === Action.Recurse) { - ts.forEachChild(node, traverse); - return; - } + if (childData === Action.Recurse) { + ts.forEachChild(node, traverse); + return; + } - const id = `${file.uri}/${childData.fullName}`.toLowerCase(); + const id = `${file.uri}/${childData.fullName}`.toLowerCase(); - // Skip duplicated tests. They won't run correctly with the way - // mocha reports them, and will error if we try to insert them. - const existing = parent.children.find(c => c.id === id); - if (existing) { - const diagnostic = new vscode.Diagnostic( - childData.range, - 'Duplicate tests cannot be run individually and will not be reported correctly by the test framework. Please rename them.', - vscode.DiagnosticSeverity.Warning - ); + // Skip duplicated tests. They won't run correctly with the way + // mocha reports them, and will error if we try to insert them. + const existing = parent.children.find(c => c.id === id); + if (existing) { + const diagnostic = new vscode.Diagnostic( + childData.range, + 'Duplicate tests cannot be run individually and will not be reported correctly by the test framework. Please rename them.', + vscode.DiagnosticSeverity.Warning + ); - diagnostic.relatedInformation = [ - new vscode.DiagnosticRelatedInformation( - new vscode.Location(existing.uri!, existing.range!), - 'First declared here' - ), - ]; + diagnostic.relatedInformation = [ + new vscode.DiagnosticRelatedInformation( + new vscode.Location(existing.uri!, existing.range!), + 'First declared here' + ), + ]; - diagnostics.push(diagnostic); - return; - } + diagnostics.push(diagnostic); + return; + } - const item = controller.createTestItem(id, childData.name, file.uri); - itemData.set(item, childData); - item.range = childData.range; - parent.children.push(item); + const item = controller.createTestItem(id, childData.name, file.uri); + itemData.set(item, childData); + item.range = childData.range; + parent.children.push(item); - if (childData instanceof TestSuite) { - parents.push({ item: item, children: [] }); - ts.forEachChild(node, traverse); - item.children.replace(parents.pop()!.children); - } - }; + if (childData instanceof TestSuite) { + parents.push({ item: item, children: [] }); + ts.forEachChild(node, traverse); + item.children.replace(parents.pop()!.children); + } + }; - ts.forEachChild(ast, traverse); - file.error = undefined; - file.children.replace(parents[0].children); - diagnosticCollection.set(this.uri, diagnostics.length ? diagnostics : undefined); - this.hasBeenRead = true; - } catch (e) { - file.error = String((e as Error).stack || (e as Error).message); - } - } + ts.forEachChild(ast, traverse); + file.error = undefined; + file.children.replace(parents[0].children); + diagnosticCollection.set(this.uri, diagnostics.length ? diagnostics : undefined); + this.hasBeenRead = true; + } catch (e) { + file.error = String((e as Error).stack || (e as Error).message); + } + } } export abstract class TestConstruct { - public fullName: string; + public fullName: string; - constructor( - public readonly name: string, - public readonly range: vscode.Range, - parent?: TestConstruct - ) { - this.fullName = parent ? `${parent.fullName} ${name}` : name; - } + constructor( + public readonly name: string, + public readonly range: vscode.Range, + parent?: TestConstruct + ) { + this.fullName = parent ? `${parent.fullName} ${name}` : name; + } } -export class TestSuite extends TestConstruct { } +export class TestSuite extends TestConstruct {} -export class TestCase extends TestConstruct { } +export class TestCase extends TestConstruct {} export type VSCodeTest = TestFile | TestSuite | TestCase; diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.test.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.test.ts index ad22e317860..c2564ca61c3 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.test.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.test.ts @@ -18,63 +18,76 @@ suite('v8CoverageWrangling', () => { const rt = new RangeCoverageTracker(); rt.cover(5, 10); rt.cover(15, 20); - assert.deepStrictEqual([...rt], [ - { start: 5, end: 10, covered: true }, - { start: 15, end: 20, covered: true }, - ]); + rt.cover(12, 13); + assert.deepStrictEqual( + [...rt], + [ + { start: 5, end: 10, covered: true }, + { start: 12, end: 13, covered: true }, + { start: 15, end: 20, covered: true }, + ] + ); }); test('covers exact', () => { const rt = new RangeCoverageTracker(); rt.uncovered(5, 10); rt.cover(5, 10); - assert.deepStrictEqual([...rt], [ - { start: 5, end: 10, covered: true }, - ]); + assert.deepStrictEqual([...rt], [{ start: 5, end: 10, covered: true }]); }); test('overlap at start', () => { const rt = new RangeCoverageTracker(); rt.uncovered(5, 10); rt.cover(2, 7); - assert.deepStrictEqual([...rt], [ - { start: 2, end: 5, covered: true }, - { start: 5, end: 7, covered: true }, - { start: 7, end: 10, covered: false }, - ]); + assert.deepStrictEqual( + [...rt], + [ + { start: 2, end: 7, covered: true }, + { start: 7, end: 10, covered: false }, + ] + ); }); test('overlap at end', () => { const rt = new RangeCoverageTracker(); rt.cover(5, 10); rt.uncovered(2, 7); - assert.deepStrictEqual([...rt], [ - { start: 2, end: 5, covered: false }, - { start: 5, end: 7, covered: true }, - { start: 7, end: 10, covered: true }, - ]); + assert.deepStrictEqual( + [...rt], + [ + { start: 2, end: 5, covered: false }, + { start: 5, end: 10, covered: true }, + ] + ); }); test('inner contained', () => { const rt = new RangeCoverageTracker(); rt.cover(5, 10); rt.uncovered(2, 12); - assert.deepStrictEqual([...rt], [ - { start: 2, end: 5, covered: false }, - { start: 5, end: 10, covered: true }, - { start: 10, end: 12, covered: false }, - ]); + assert.deepStrictEqual( + [...rt], + [ + { start: 2, end: 5, covered: false }, + { start: 5, end: 10, covered: true }, + { start: 10, end: 12, covered: false }, + ] + ); }); test('outer contained', () => { const rt = new RangeCoverageTracker(); rt.uncovered(5, 10); rt.cover(7, 9); - assert.deepStrictEqual([...rt], [ - { start: 5, end: 7, covered: false }, - { start: 7, end: 9, covered: true }, - { start: 9, end: 10, covered: false }, - ]); + assert.deepStrictEqual( + [...rt], + [ + { start: 5, end: 7, covered: false }, + { start: 7, end: 9, covered: true }, + { start: 9, end: 10, covered: false }, + ] + ); }); test('boundary touching', () => { @@ -82,25 +95,62 @@ suite('v8CoverageWrangling', () => { rt.uncovered(5, 10); rt.cover(10, 15); rt.uncovered(15, 20); - assert.deepStrictEqual([...rt], [ - { start: 5, end: 10, covered: false }, - { start: 10, end: 15, covered: true }, - { start: 15, end: 20, covered: false }, - ]); + assert.deepStrictEqual( + [...rt], + [ + { start: 5, end: 10, covered: false }, + { start: 10, end: 15, covered: true }, + { start: 15, end: 20, covered: false }, + ] + ); }); - test('initializeBlock', () => { - const rt = RangeCoverageTracker.initializeBlock([ - { count: 1, startOffset: 5, endOffset: 30 }, - { count: 1, startOffset: 8, endOffset: 10 }, - { count: 0, startOffset: 15, endOffset: 20 }, - ]); + suite('initializeBlock', () => { + test('simple tree', () => { + const rt = RangeCoverageTracker.initializeBlocks([ + { + functionName: 'outer', + isBlockCoverage: true, + ranges: [ + { count: 1, startOffset: 5, endOffset: 30 }, + { count: 1, startOffset: 8, endOffset: 10 }, + { count: 0, startOffset: 15, endOffset: 20 }, + ], + }, + ]); - assert.deepStrictEqual([...rt], [ - { start: 5, end: 15, covered: true }, - { start: 15, end: 20, covered: false }, - { start: 20, end: 30, covered: true }, - ]); + assert.deepStrictEqual( + [...rt], + [ + { start: 5, end: 15, covered: true }, + { start: 15, end: 20, covered: false }, + { start: 20, end: 30, covered: true }, + ] + ); + }); + + test('separate branches', () => { + const rt = RangeCoverageTracker.initializeBlocks([ + { + functionName: 'outer', + isBlockCoverage: true, + ranges: [ + { count: 1, startOffset: 5, endOffset: 8 }, + { count: 1, startOffset: 10, endOffset: 12 }, + { count: 0, startOffset: 15, endOffset: 20 }, + ], + }, + ]); + + assert.deepStrictEqual( + [...rt], + [ + { start: 5, end: 8, covered: true }, + { start: 10, end: 12, covered: true }, + { start: 15, end: 20, covered: false }, + ] + ); + }); }); }); }); diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.ts index 9fbecbd8ba5..ede638430ba 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.ts @@ -30,7 +30,6 @@ export interface IScriptCoverage { functions: IV8FunctionCoverage[]; } - export class RangeCoverageTracker implements Iterable { /** * A noncontiguous, non-overlapping, ordered set of ranges and whether @@ -41,26 +40,43 @@ export class RangeCoverageTracker implements Iterable { /** * Adds a coverage tracker initialized for a function with {@link isBlockCoverage} set to true. */ - public static initializeBlock(ranges: IV8CoverageRange[]) { - let start = ranges[0].startOffset; + public static initializeBlocks(fns: IV8FunctionCoverage[]) { const rt = new RangeCoverageTracker(); - if (!ranges[0].count) { - rt.uncovered(start, ranges[0].endOffset); - return rt; - } - for (let i = 1; i < ranges.length; i++) { - const range = ranges[i]; - if (range.count) { - continue; + let start = 0; + const stack: IV8CoverageRange[] = []; + + // note: comes pre-sorted from V8 + for (const { ranges } of fns) { + for (const range of ranges) { + while (stack.length && stack[stack.length - 1].endOffset < range.startOffset) { + const last = stack.pop()!; + rt.setCovered(start, last.endOffset, last.count > 0); + start = last.endOffset; + } + + if (range.startOffset > start && stack.length) { + rt.setCovered(start, range.startOffset, !!stack[stack.length - 1].count); + } + + start = range.startOffset; + stack.push(range); } - - rt.cover(start, range.startOffset); - rt.uncovered(range.startOffset, range.endOffset); - start = range.endOffset; } - rt.cover(start, ranges[0].endOffset); + while (stack.length) { + const last = stack.pop()!; + rt.setCovered(start, last.endOffset, last.count > 0); + start = last.endOffset; + } + + return rt; + } + + /** Makes a copy of the range tracker. */ + public clone() { + const rt = new RangeCoverageTracker(); + rt.ranges = this.ranges.slice(); return rt; } @@ -79,6 +95,12 @@ export class RangeCoverageTracker implements Iterable { return this.ranges[Symbol.iterator](); } + /** + * Marks the given character range as being covered or uncovered. + * + * todo@connor4312: this is a hot path is could probably be optimized to + * avoid rebuilding the array. Maybe with a nice tree structure? + */ public setCovered(start: number, end: number, covered: boolean) { const newRanges: ICoverageRange[] = []; let i = 0; @@ -86,38 +108,53 @@ export class RangeCoverageTracker implements Iterable { newRanges.push(this.ranges[i]); } - newRanges.push({ start, end, covered }); + const push = (range: ICoverageRange) => { + const last = newRanges.length && newRanges[newRanges.length - 1]; + if (last && last.end === range.start && last.covered === range.covered) { + last.end = range.end; + } else { + newRanges.push(range); + } + }; + + push({ start, end, covered }); + for (; i < this.ranges.length; i++) { const range = this.ranges[i]; const last = newRanges[newRanges.length - 1]; - if (range.start < last.start && range.end > last.end) { + if (range.start === last.start && range.end === last.end) { + // ranges are equal: + last.covered ||= range.covered; + } else if (range.end < last.start || range.start > last.end) { + // ranges don't overlap + push(range); + } else if (range.start < last.start && range.end > last.end) { // range contains last: newRanges.pop(); - newRanges.push({ start: range.start, end: last.start, covered: range.covered }); - newRanges.push({ start: last.start, end: last.end, covered: range.covered || last.covered }); - newRanges.push({ start: last.end, end: range.end, covered: range.covered }); - } else if (range.start > last.start && range.end <= last.end) { + push({ start: range.start, end: last.start, covered: range.covered }); + push({ start: last.start, end: last.end, covered: range.covered || last.covered }); + push({ start: last.end, end: range.end, covered: range.covered }); + } else if (range.start >= last.start && range.end <= last.end) { // last contains range: newRanges.pop(); - newRanges.push({ start: last.start, end: range.start, covered: last.covered }); - newRanges.push({ start: range.start, end: range.end, covered: range.covered || last.covered }); - newRanges.push({ start: range.end, end: last.end, covered: last.covered }); + push({ start: last.start, end: range.start, covered: last.covered }); + push({ start: range.start, end: range.end, covered: range.covered || last.covered }); + push({ start: range.end, end: last.end, covered: last.covered }); } else if (range.start < last.start && range.end <= last.end) { // range overlaps start of last: newRanges.pop(); - newRanges.push({ start: range.start, end: last.start, covered: range.covered }); - newRanges.push({ start: last.start, end: range.end, covered: range.covered || last.covered }); - newRanges.push({ start: range.end, end: last.end, covered: last.covered }); - } else if (range.start > last.start && range.end > last.end) { + push({ start: range.start, end: last.start, covered: range.covered }); + push({ start: last.start, end: range.end, covered: range.covered || last.covered }); + push({ start: range.end, end: last.end, covered: last.covered }); + } else if (range.start >= last.start && range.end > last.end) { // range overlaps end of last: newRanges.pop(); - newRanges.push({ start: last.start, end: range.start, covered: last.covered }); - newRanges.push({ start: range.start, end: last.end, covered: range.covered || last.covered }); - newRanges.push({ start: last.end, end: range.end, covered: range.covered }); + push({ start: last.start, end: range.start, covered: last.covered }); + push({ start: range.start, end: last.end, covered: range.covered || last.covered }); + push({ start: last.end, end: range.end, covered: range.covered }); } else { - // ranges are equal: - last.covered ||= range.covered; + throw new Error('unreachable'); } } @@ -127,14 +164,24 @@ export class RangeCoverageTracker implements Iterable { export class OffsetToPosition { /** Line numbers to byte offsets. */ - public readonly lines: number[] = []; + public readonly lines: number[] = []; - constructor(public readonly source: string) { - this.lines.push(0); - for (let i = source.indexOf('\n'); i !== -1; i = source.indexOf('\n', i + 1)) { - this.lines.push(i + 1); - } - } + public readonly totalLength: number; + + constructor(source: string) { + this.lines.push(0); + for (let i = source.indexOf('\n'); i !== -1; i = source.indexOf('\n', i + 1)) { + this.lines.push(i + 1); + } + this.totalLength = source.length; + } + + public getLineLength(lineNumber: number): number { + return ( + (lineNumber < this.lines.length - 1 ? this.lines[lineNumber + 1] - 1 : this.totalLength) - + this.lines[lineNumber] + ); + } /** * Gets the line the offset appears on. @@ -154,11 +201,11 @@ export class OffsetToPosition { return low - 1; } - /** - * Converts from a file offset to a base 0 line/column . - */ - public convert(offset: number): { line: number; column: number } { + /** + * Converts from a file offset to a base 0 line/column . + */ + public toLineColumn(offset: number): { line: number; column: number } { const line = this.getLineOfOffset(offset); return { line: line, column: offset - this.lines[line] }; - } + } } diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/vscodeTestRunner.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/vscodeTestRunner.ts index e8c36118c65..5d73928aed5 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/vscodeTestRunner.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/vscodeTestRunner.ts @@ -24,7 +24,7 @@ const ATTACH_CONFIG_NAME = 'Attach to VS Code'; const DEBUG_TYPE = 'pwa-chrome'; export abstract class VSCodeTestRunner { - constructor(protected readonly repoLocation: vscode.WorkspaceFolder) { } + constructor(protected readonly repoLocation: vscode.WorkspaceFolder) {} public async run(baseArgs: ReadonlyArray, filter?: ReadonlyArray) { const args = this.prepareArguments(baseArgs, filter); @@ -303,5 +303,5 @@ export const PlatformTestRunner = process.platform === 'win32' ? WindowsTestRunner : process.platform === 'darwin' - ? DarwinTestRunner - : PosixTestRunner; + ? DarwinTestRunner + : PosixTestRunner; diff --git a/package.json b/package.json index a421fc39606..05d17c70176 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.90.0", - "distro": "75eed367612c65f6edd69c54373da84495e0d0b8", + "distro": "fec0321a3182f40f776709b5ac183c59a120a2b6", "author": { "name": "Microsoft Corporation" }, diff --git a/src/vs/workbench/api/common/extHostTesting.ts b/src/vs/workbench/api/common/extHostTesting.ts index 7e50f1fa9fc..7865e9a2d3d 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/src/vs/workbench/api/common/extHostTesting.ts @@ -491,8 +491,8 @@ class TestRunTracker extends Disposable { /** Gets details for a previously-emitted coverage object. */ public getCoverageDetails(id: string, token: CancellationToken) { - const [, taskId, covId] = TestId.fromString(id).path; /** runId, taskId, URI */ - const coverage = this.publishedCoverage.get(covId); + const [, taskId] = TestId.fromString(id).path; /** runId, taskId, URI */ + const coverage = this.publishedCoverage.get(id); if (!coverage) { return []; } @@ -573,11 +573,11 @@ class TestRunTracker extends Disposable { } const uriStr = coverage.uri.toString(); - const id = new TestId(testItemIdPart + const id = new TestId(testItemIdPart !== undefined ? [runId, taskId, uriStr, String(testItemIdPart)] : [runId, taskId, uriStr], ).toString(); - this.publishedCoverage.set(uriStr, coverage); + this.publishedCoverage.set(id, coverage); this.proxy.$appendCoverage(runId, taskId, Convert.TestCoverage.fromFile(ctrlId, id, coverage)); }, //#region state mutation From f8d5fca37d8810a25d240956442b106f6b89fc70 Mon Sep 17 00:00:00 2001 From: David Dossett Date: Thu, 9 May 2024 13:48:57 -0700 Subject: [PATCH 079/357] Tweak chat agent hover styles --- .../workbench/contrib/chat/browser/media/chatAgentHover.css | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/media/chatAgentHover.css b/src/vs/workbench/contrib/chat/browser/media/chatAgentHover.css index edbe3ab0cf6..a3573b367b4 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatAgentHover.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatAgentHover.css @@ -10,7 +10,8 @@ .chat-agent-hover-header { display: flex; - gap: 5px; + gap: 8px; + margin-bottom: 4px; } .chat-agent-hover-icon img, @@ -55,13 +56,14 @@ } .chat-agent-hover-header .chat-agent-hover-name { - font-size: 15px; + font-size: 14px; font-weight: 600; } .chat-agent-hover-extension { display: flex; gap: 6px; + color: var(--vscode-descriptionForeground); } .chat-agent-hover.noExtensionName .chat-agent-hover-separator, From 5a46e2ef37c6883ab295c1fc466e58b11656eb88 Mon Sep 17 00:00:00 2001 From: David Dossett Date: Thu, 9 May 2024 15:34:42 -0700 Subject: [PATCH 080/357] Message header font spacing tweak --- src/vs/workbench/contrib/chat/browser/media/chat.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 15de8a3352b..a00f97a868c 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -39,12 +39,12 @@ .interactive-item-container .header .user { display: flex; align-items: center; - gap: 6px; + gap: 8px; } .interactive-item-container .header .username { margin: 0; - font-size: 12px; + font-size: 13px; font-weight: 600; } From dc45ddef952db43309db62d442176c36b4b52ccd Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Thu, 9 May 2024 15:59:58 -0700 Subject: [PATCH 081/357] Remove requirement that there can be only one account (#212398) This allows each set of scopes to have one account associated with it. --- .../github-authentication/src/github.ts | 35 ++----------------- 1 file changed, 3 insertions(+), 32 deletions(-) diff --git a/extensions/github-authentication/src/github.ts b/extensions/github-authentication/src/github.ts index 3d73bfb7656..c603455facf 100644 --- a/extensions/github-authentication/src/github.ts +++ b/extensions/github-authentication/src/github.ts @@ -11,7 +11,7 @@ import { PromiseAdapter, arrayEquals, promiseFromEvent } from './common/utils'; import { ExperimentationTelemetry } from './common/experimentationService'; import { Log } from './common/logger'; import { crypto } from './node/crypto'; -import { CANCELLATION_ERROR, TIMED_OUT_ERROR, USER_CANCELLATION_ERROR } from './common/errors'; +import { TIMED_OUT_ERROR, USER_CANCELLATION_ERROR } from './common/errors'; interface SessionData { id: string; @@ -298,42 +298,13 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid const sessions = await this._sessionsPromise; + const accounts = new Set(sessions.map(session => session.account.label)); + const existingLogin = accounts.size <= 1 ? sessions[0]?.account.label : await vscode.window.showQuickPick([...accounts], { placeHolder: 'Choose an account that you would like to log in to' }); const scopeString = sortedScopes.join(' '); - const existingLogin = sessions[0]?.account.label; const token = await this._githubServer.login(scopeString, existingLogin); const session = await this.tokenToSession(token, scopes); this.afterSessionLoad(session); - if (sessions.some(s => s.account.id !== session.account.id)) { - const otherAccountsIndexes = new Array(); - const otherAccountsLabels = new Set(); - for (let i = 0; i < sessions.length; i++) { - if (sessions[i].account.id !== session.account.id) { - otherAccountsIndexes.push(i); - otherAccountsLabels.add(sessions[i].account.label); - } - } - const proceed = vscode.l10n.t("Continue"); - const labelstr = [...otherAccountsLabels].join(', '); - const result = await vscode.window.showInformationMessage( - vscode.l10n.t({ - message: "You are logged into another account already ({0}).\n\nDo you want to log out of that account and log in to '{1}' instead?", - comment: ['{0} is a comma-separated list of account names. {1} is the account name to log into.'], - args: [labelstr, session.account.label] - }), - { modal: true }, - proceed - ); - if (result !== proceed) { - throw new Error(CANCELLATION_ERROR); - } - - // Remove other accounts - for (const i of otherAccountsIndexes) { - sessions.splice(i, 1); - } - } - const sessionIndex = sessions.findIndex(s => s.id === session.id || arrayEquals([...s.scopes].sort(), sortedScopes)); if (sessionIndex > -1) { sessions.splice(sessionIndex, 1, session); From d1555a7b21ac6e7e267743a42270c93e57ceb4e7 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 9 May 2024 16:05:08 -0700 Subject: [PATCH 082/357] Get rid of "GitHub Copilot" header and add a participant "fullName" --- .../api/browser/mainThreadChatAgents2.ts | 7 +- .../workbench/api/common/extHost.api.impl.ts | 4 +- .../workbench/api/common/extHost.protocol.ts | 9 +- .../api/common/extHostChatAgents2.ts | 15 +-- .../contrib/chat/browser/chatListRenderer.ts | 101 ++++-------------- .../browser/chatParticipantContributions.ts | 8 +- .../contrib/chat/browser/media/chat.css | 17 --- .../contrib/chat/common/chatAgents.ts | 2 +- .../contrib/chat/common/chatModel.ts | 4 +- .../common/chatParticipantContribTypes.ts | 1 + .../contrib/chat/common/chatViewModel.ts | 6 +- .../ChatService_can_deserialize.0.snap | 2 +- .../ChatService_can_serialize.0.snap | 2 +- .../ChatService_can_serialize.1.snap | 12 +-- .../chat/test/common/chatService.test.ts | 4 +- .../browser/commandsQuickAccess.ts | 2 +- ...ode.proposed.chatParticipantAdditions.d.ts | 12 ++- ...scode.proposed.defaultChatParticipant.d.ts | 5 - 18 files changed, 71 insertions(+), 142 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index 57f98dd25bf..385f5ac4e93 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -18,7 +18,7 @@ import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeat import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; -import { ExtHostChatAgentsShape2, ExtHostContext, IChatProgressDto, IExtensionChatAgentMetadata, MainContext, MainThreadChatAgentsShape2 } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostChatAgentsShape2, ExtHostContext, IChatProgressDto, IDynamicChatAgentProps, IExtensionChatAgentMetadata, MainContext, MainThreadChatAgentsShape2 } from 'vs/workbench/api/common/extHost.protocol'; 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'; @@ -96,7 +96,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA this._chatService.transferChatSession({ sessionId, inputValue }, URI.revive(toWorkspace)); } - $registerAgent(handle: number, extension: ExtensionIdentifier, id: string, metadata: IExtensionChatAgentMetadata, dynamicProps: { name: string; description: string; publisherDisplayName: string } | undefined): void { + $registerAgent(handle: number, extension: ExtensionIdentifier, id: string, metadata: IExtensionChatAgentMetadata, dynamicProps: IDynamicChatAgentProps | undefined): void { const staticAgentRegistration = this._chatAgentService.getAgent(id); if (!staticAgentRegistration && !dynamicProps) { if (this._chatAgentService.getAgentsByName(id).length) { @@ -143,7 +143,8 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA extensionId: extension, extensionDisplayName: extensionDescription?.displayName ?? extension.value, extensionPublisherId: extensionDescription?.publisher ?? '', - publisherDisplayName: dynamicProps.publisherDisplayName, + publisherDisplayName: dynamicProps.publisherName, + fullName: dynamicProps.fullName, metadata: revive(metadata), slashCommands: [], locations: [ChatAgentLocation.Panel] // TODO all dynamic participants are panel only? diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 0a622e4d95a..186d496596b 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1420,9 +1420,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'chatParticipant'); return extHostChatAgents2.createChatAgent(extension, id, handler); }, - createDynamicChatParticipant(id: string, name: string, publisherName: string, description: string, handler: vscode.ChatExtendedRequestHandler): vscode.ChatParticipant { + createDynamicChatParticipant(id: string, dynamicProps: vscode.DynamicChatParticipantProps, handler: vscode.ChatExtendedRequestHandler): vscode.ChatParticipant { checkProposedApiEnabled(extension, 'chatParticipantAdditions'); - return extHostChatAgents2.createDynamicChatAgent(extension, id, name, publisherName, description, handler); + return extHostChatAgents2.createDynamicChatAgent(extension, id, dynamicProps, handler); } }; diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 190e7c4b45e..a8750532975 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1232,8 +1232,15 @@ export interface IExtensionChatAgentMetadata extends Dto { hasFollowups?: boolean; } +export interface IDynamicChatAgentProps { + name: string; + publisherName: string; + description?: string; + fullName?: string; +} + export interface MainThreadChatAgentsShape2 extends IDisposable { - $registerAgent(handle: number, extension: ExtensionIdentifier, id: string, metadata: IExtensionChatAgentMetadata, dynamicProps: { name: string; description: string; publisherDisplayName: string } | undefined): void; + $registerAgent(handle: number, extension: ExtensionIdentifier, id: string, metadata: IExtensionChatAgentMetadata, dynamicProps: IDynamicChatAgentProps | 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 78a4076d962..11fcd8875c6 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -258,12 +258,12 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS return agent.apiAgent; } - createDynamicChatAgent(extension: IExtensionDescription, id: string, name: string, publisherName: string, description: string, handler: vscode.ChatExtendedRequestHandler): vscode.ChatParticipant { + createDynamicChatAgent(extension: IExtensionDescription, id: string, dynamicProps: vscode.DynamicChatParticipantProps, 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, { isSticky: true } satisfies IExtensionChatAgentMetadata, { name, description, publisherDisplayName: publisherName }); + this._proxy.$registerAgent(handle, extension.identifier, id, { isSticky: true } satisfies IExtensionChatAgentMetadata, dynamicProps); return agent.apiAgent; } @@ -439,7 +439,6 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS class ExtHostChatAgent { private _followupProvider: vscode.ChatFollowupProvider | undefined; - private _fullName: string | undefined; private _iconPath: vscode.Uri | { light: vscode.Uri; dark: vscode.Uri } | vscode.ThemeIcon | undefined; private _isDefault: boolean | undefined; private _helpTextPrefix: string | vscode.MarkdownString | undefined; @@ -535,7 +534,6 @@ class ExtHostChatAgent { updateScheduled = true; queueMicrotask(() => { this._proxy.$updateAgent(this._handle, { - fullName: this._fullName, icon: !this._iconPath ? undefined : this._iconPath instanceof URI ? this._iconPath : 'light' in this._iconPath ? this._iconPath.light : @@ -561,15 +559,6 @@ class ExtHostChatAgent { get id() { return that.id; }, - get fullName() { - checkProposedApiEnabled(that.extension, 'defaultChatParticipant'); - return that._fullName ?? that.extension.displayName ?? that.extension.name; - }, - set fullName(v) { - checkProposedApiEnabled(that.extension, 'defaultChatParticipant'); - that._fullName = v; - updateMetadataSoon(); - }, get iconPath() { return that._iconPath; }, diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index ca31e35b677..e4d158b307f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -16,7 +16,6 @@ import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; import { IAsyncDataSource, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; import { IAction } from 'vs/base/common/actions'; import { distinct } from 'vs/base/common/arrays'; -import { disposableTimeout } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Codicon } from 'vs/base/common/codicons'; import { Emitter, Event } from 'vs/base/common/event'; @@ -26,12 +25,11 @@ import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/ import { ResourceMap } from 'vs/base/common/map'; import { FileAccess, Schemas, matchesSomeScheme } from 'vs/base/common/network'; import { clamp } from 'vs/base/common/numbers'; -import { IObservable, autorun, constObservable } from 'vs/base/common/observable'; +import { autorun } from 'vs/base/common/observable'; import { basename } from 'vs/base/common/path'; import { basenameOrAuthority } from 'vs/base/common/resources'; import { equalsIgnoreCase } from 'vs/base/common/strings'; import { ThemeIcon } from 'vs/base/common/themables'; -import { isUndefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { IMarkdownRenderResult, MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; @@ -66,10 +64,10 @@ import { ChatFollowups } from 'vs/workbench/contrib/chat/browser/chatFollowups'; import { ChatMarkdownDecorationsRenderer } from 'vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer'; import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions'; import { ChatCodeBlockContentProvider, CodeBlockPart, CodeCompareBlockPart, ICodeBlockData, ICodeCompareBlockData, ICodeCompareBlockDiffData, localFileLanguageId, parseLocalFileData } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; -import { ChatAgentLocation, IChatAgentMetadata, IChatAgentNameService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatAgentLocation, IChatAgentMetadata } from 'vs/workbench/contrib/chat/common/chatAgents'; import { CONTEXT_CHAT_RESPONSE_SUPPORT_ISSUE_REPORTING, CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_DETECTED_AGENT_COMMAND, CONTEXT_RESPONSE_FILTERED, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatProgressRenderableResponseContent, IChatTextEditGroup } from 'vs/workbench/contrib/chat/common/chatModel'; -import { chatAgentLeader, chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import { chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatCommandButton, IChatConfirmation, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseProgressFileTreeData, IChatSendRequestOptions, IChatService, IChatTask, IChatWarningMessage, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import { IChatProgressMessageRenderData, IChatRenderData, IChatResponseMarkdownRenderData, IChatResponseViewModel, IChatTaskRenderData, IChatWelcomeMessageViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; @@ -88,7 +86,6 @@ interface IChatListItemTemplate { readonly rowContainer: HTMLElement; readonly titleToolbar?: MenuWorkbenchToolBar; readonly avatarContainer: HTMLElement; - readonly agentAvatarContainer: HTMLElement; readonly username: HTMLElement; readonly detail: HTMLElement; readonly value: HTMLElement; @@ -159,7 +156,6 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer = constObservable(undefined); - - if (element.agent && !element.agent.isDefault) { - const name = element.agent.name; - agentName = this.chatAgentNameService.getAgentNameRestriction(element.agent) - .map(allowed => allowed ? name : name); // TODO - } - templateData.elementDisposables.add(autorun(reader => { - this._renderDetail(element, agentName.read(reader), templateData); + this._renderDetail(element, templateData); })); } - private _renderDetail(element: IChatResponseViewModel, agentName: string | undefined, templateData: IChatListItemTemplate): void { + private _renderDetail(element: IChatResponseViewModel, templateData: IChatListItemTemplate): void { let progressMsg: string = ''; - if (!isUndefined(agentName)) { - let usingMsg = chatAgentLeader + agentName; - if (element.slashCommand) { - usingMsg += ` ${chatSubcommandLeader}${element.slashCommand.name}`; - } - + if (element.slashCommand && element.agentOrSlashCommandDetected) { + const usingMsg = `${chatSubcommandLeader}${element.slashCommand.name}`; if (element.isComplete) { progressMsg = localize('usedAgent', "used {0}", usingMsg); } else { progressMsg = localize('usingAgent', "using {0}", usingMsg); } - } else if (element.agentOrSlashCommandDetected) { - const usingMsg: string[] = []; - if (!isUndefined(agentName)) { - usingMsg.push(chatAgentLeader + agentName); - } - if (element.slashCommand) { - usingMsg.push(chatSubcommandLeader + element.slashCommand.name); - } - if (usingMsg.length) { - if (element.isComplete) { - progressMsg = localize('usedAgent', "used {0}", usingMsg.join(' ')); - } else { - progressMsg = localize('usingAgent', "using {0}", usingMsg.join(' ')); - } - } } else if (!element.isComplete) { progressMsg = GeneratingPhrase; } @@ -444,53 +412,28 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer('img.icon'); - avatarImgIcon.src = FileAccess.uriToBrowserUri(element.avatarIcon).toString(true); - templateData.avatarContainer.replaceChildren(dom.$('.avatar', undefined, avatarImgIcon)); + const icon = isResponseVM(element) ? + this.getAgentIcon(element.agent?.metadata) : + (element.avatarIcon ?? Codicon.account); + if (icon instanceof URI) { + const avatarIcon = dom.$('img.icon'); + avatarIcon.src = FileAccess.uriToBrowserUri(icon).toString(true); + templateData.avatarContainer.replaceChildren(dom.$('.avatar', undefined, avatarIcon)); } else { - const defaultIcon = isRequestVM(element) ? Codicon.account : Codicon.copilot; - const icon = element.avatarIcon ?? defaultIcon; const avatarIcon = dom.$(ThemeIcon.asCSSSelector(icon)); templateData.avatarContainer.replaceChildren(dom.$('.avatar.codicon-avatar', undefined, avatarIcon)); } - - if (isResponseVM(element) && element.agent && !element.agent.isDefault) { - dom.show(templateData.agentAvatarContainer); - const icon = this.getAgentIcon(element.agent.metadata); - if (icon instanceof URI) { - const avatarIcon = dom.$('img.icon'); - avatarIcon.src = FileAccess.uriToBrowserUri(icon).toString(true); - templateData.agentAvatarContainer.replaceChildren(dom.$('.avatar', undefined, avatarIcon)); - } else if (icon) { - const avatarIcon = dom.$(ThemeIcon.asCSSSelector(icon)); - templateData.agentAvatarContainer.replaceChildren(dom.$('.avatar.codicon-avatar', undefined, avatarIcon)); - } else { - dom.hide(templateData.agentAvatarContainer); - return; - } - - templateData.agentAvatarContainer.classList.toggle('complete', element.isComplete); - if (!element.agentAvatarHasBeenRendered && !element.isComplete) { - element.agentAvatarHasBeenRendered = true; - templateData.agentAvatarContainer.classList.remove('loading'); - templateData.elementDisposables.add(disposableTimeout(() => { - templateData.agentAvatarContainer.classList.toggle('loading', !element.isComplete); - }, 100)); - } else { - templateData.agentAvatarContainer.classList.toggle('loading', !element.isComplete); - } - } else { - dom.hide(templateData.agentAvatarContainer); - } } - private getAgentIcon(agent: IChatAgentMetadata): URI | ThemeIcon | undefined { - if (agent.themeIcon) { + private getAgentIcon(agent: IChatAgentMetadata | undefined): URI | ThemeIcon { + if (agent?.themeIcon) { return agent.themeIcon; + } else if (agent?.iconDark && this.themeService.getColorTheme().type === ColorScheme.DARK) { + return agent.iconDark; + } else if (agent?.icon) { + return agent.icon; } else { - return this.themeService.getColorTheme().type === ColorScheme.DARK && agent.iconDark ? agent.iconDark : - agent.icon; + return Codicon.copilot; } } diff --git a/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts b/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts index c50724ba6d1..f5132a7ab55 100644 --- a/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts @@ -39,7 +39,12 @@ const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.regi type: 'string' }, name: { - description: localize('chatParticipantName', "User-facing display name for this chat participant. The user will use '@' with this name to invoke the participant."), + description: localize('chatParticipantName', "User-facing name for this chat participant. The user will use '@' with this name to invoke the participant."), + type: 'string', + pattern: '^[\w0-9_-]+$' + }, + fullName: { + markdownDescription: localize('chatParticipantFullName', "The full name of this chat participant, which is shown as the label for responses coming from this participant. If not provided, {0} is used.", '`name`'), type: 'string' }, description: { @@ -216,6 +221,7 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { sampleRequest: providerDescriptor.sampleRequest, }, name: providerDescriptor.name, + fullName: providerDescriptor.fullName, isDefault: providerDescriptor.isDefault, defaultImplicitVariables: providerDescriptor.defaultImplicitVariables, locations: isNonEmptyArray(providerDescriptor.locations) ? diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 15de8a3352b..b6c75bca71e 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -124,23 +124,6 @@ font-size: 14px; } -.interactive-item-container .header .agent-avatar-container { - margin-left: -30px; - transition: margin 0.15s ease-out; - transition-delay: 0.5s; - z-index: -1; -} - -.interactive-item-container .header .agent-avatar-container.loading { - margin-left: 0px; - z-index: 1; -} - -.interactive-item-container .header .agent-avatar-container.complete { - margin-left: -12px; - z-index: 1; -} - .monaco-list-row:not(.focused) .interactive-item-container:not(:hover) .header .monaco-toolbar, .monaco-list:not(:focus-within) .monaco-list-row .interactive-item-container:not(:hover) .header .monaco-toolbar, .monaco-list-row:not(.focused) .interactive-item-container:not(:hover) .header .monaco-toolbar .action-label, diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index 9bf43b3db5c..2fd60cfb727 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -57,6 +57,7 @@ export namespace ChatAgentLocation { export interface IChatAgentData { id: string; name: string; + fullName?: string; description?: string; extensionId: ExtensionIdentifier; extensionPublisherId: string; @@ -100,7 +101,6 @@ export interface IChatAgentMetadata { helpTextVariablesPrefix?: string | IMarkdownString; helpTextPostfix?: string | IMarkdownString; isSecondary?: boolean; // Invoked by ctrl/cmd+enter - fullName?: string; icon?: URI; iconDark?: URI; themeIcon?: ThemeIcon; diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 31fb8e81f3f..d4649b0acd7 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -580,7 +580,7 @@ export class ChatModel extends Disposable implements IChatModel { get responderUsername(): string { return (this._defaultAgent ? - this._defaultAgent.metadata.fullName : + this._defaultAgent.fullName : this.initialData?.responderUsername) ?? ''; } @@ -956,7 +956,7 @@ export class ChatWelcomeMessageModel implements IChatWelcomeMessageModel { } public get username(): string { - return this.chatAgentService.getDefaultAgent(ChatAgentLocation.Panel)?.metadata.fullName ?? ''; + return this.chatAgentService.getDefaultAgent(ChatAgentLocation.Panel)?.fullName ?? ''; } public get avatarIcon(): ThemeIcon | undefined { diff --git a/src/vs/workbench/contrib/chat/common/chatParticipantContribTypes.ts b/src/vs/workbench/contrib/chat/common/chatParticipantContribTypes.ts index 4c4449bbaf9..e6d695c3824 100644 --- a/src/vs/workbench/contrib/chat/common/chatParticipantContribTypes.ts +++ b/src/vs/workbench/contrib/chat/common/chatParticipantContribTypes.ts @@ -17,6 +17,7 @@ export type RawChatParticipantLocation = 'panel' | 'terminal' | 'notebook'; export interface IRawChatParticipantContribution { id: string; name: string; + fullName: string; description?: string; isDefault?: boolean; isSticky?: boolean; diff --git a/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/chatViewModel.ts index 3f34f8f239d..96b4ef6d464 100644 --- a/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatViewModel.ts @@ -137,7 +137,6 @@ export interface IChatResponseViewModel { readonly result?: IChatAgentResult; readonly contentUpdateTimings?: IChatLiveUpdateData; renderData?: IChatResponseRenderData; - agentAvatarHasBeenRendered?: boolean; currentRenderedHeight: number | undefined; setVote(vote: InteractiveSessionVoteDirection): void; usedReferencesExpanded?: boolean; @@ -375,7 +374,9 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi } get username() { - return this._model.username; + return this.agent ? + (this.agent.fullName || this.agent.name) : + this._model.username; } get avatarIcon() { @@ -443,7 +444,6 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi } renderData: IChatResponseRenderData | undefined = undefined; - agentAvatarHasBeenRendered?: boolean; currentRenderedHeight: number | undefined; private _usedReferencesExpanded: boolean | undefined; 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 index 83ff5c45284..383288306cb 100644 --- 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 @@ -1,7 +1,7 @@ { requesterUsername: "test", requesterAvatarIconUri: undefined, - responderUsername: "test", + responderUsername: "", responderAvatarIconUri: undefined, initialLocation: "panel", welcomeMessage: undefined, 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 index 75fe21b2b15..a0e87754398 100644 --- 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 @@ -1,7 +1,7 @@ { requesterUsername: "test", requesterAvatarIconUri: undefined, - responderUsername: "test", + responderUsername: "", responderAvatarIconUri: undefined, initialLocation: "panel", welcomeMessage: undefined, 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 index c4e365a2a54..cf9cc42dfc6 100644 --- 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 @@ -1,7 +1,7 @@ { requesterUsername: "test", requesterAvatarIconUri: undefined, - responderUsername: "test", + responderUsername: "", responderAvatarIconUri: undefined, initialLocation: "panel", welcomeMessage: undefined, @@ -31,10 +31,7 @@ publisherDisplayName: "", extensionDisplayName: "", locations: [ "panel" ], - metadata: { - requester: { name: "test" }, - fullName: "test" - }, + metadata: { requester: { name: "test" } }, slashCommands: [ ] }, kind: "agent" @@ -73,10 +70,7 @@ publisherDisplayName: "", extensionDisplayName: "", locations: [ "panel" ], - metadata: { - requester: { name: "test" }, - fullName: "test" - }, + metadata: { requester: { name: "test" } }, slashCommands: [ ] }, slashCommand: undefined, 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 bcca1f9bc98..513f786bb59 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts @@ -95,7 +95,7 @@ suite('ChatService', () => { testDisposables.add(chatAgentService.registerAgent('testAgent', { name: 'testAgent', id: 'testAgent', isDefault: true, extensionId: nullExtensionDescription.identifier, extensionPublisherId: '', publisherDisplayName: '', extensionDisplayName: '', locations: [ChatAgentLocation.Panel], metadata: {}, slashCommands: [] })); testDisposables.add(chatAgentService.registerAgent(chatAgentWithUsedContextId, { name: chatAgentWithUsedContextId, id: chatAgentWithUsedContextId, extensionId: nullExtensionDescription.identifier, extensionPublisherId: '', publisherDisplayName: '', extensionDisplayName: '', locations: [ChatAgentLocation.Panel], metadata: {}, slashCommands: [] })); testDisposables.add(chatAgentService.registerAgentImplementation('testAgent', agent)); - chatAgentService.updateAgent('testAgent', { requester: { name: 'test' }, fullName: 'test' }); + chatAgentService.updateAgent('testAgent', { requester: { name: 'test' } }); }); test('retrieveSession', async () => { @@ -132,7 +132,7 @@ suite('ChatService', () => { test('can serialize', async () => { testDisposables.add(chatAgentService.registerAgentImplementation(chatAgentWithUsedContextId, chatAgentWithUsedContext)); - chatAgentService.updateAgent(chatAgentWithUsedContextId, { requester: { name: 'test' }, fullName: 'test' }); + chatAgentService.updateAgent(chatAgentWithUsedContextId, { requester: { name: 'test' } }); const testService = testDisposables.add(instantiationService.createInstance(ChatService)); const model = testDisposables.add(testService.startSession(ChatAgentLocation.Panel, CancellationToken.None)); diff --git a/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts b/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts index 59ec31d81d6..17b7b7ac490 100644 --- a/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts +++ b/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts @@ -176,7 +176,7 @@ export class CommandsQuickAccessProvider extends AbstractEditorCommandsQuickAcce const defaultAgent = this.chatAgentService.getDefaultAgent(ChatAgentLocation.Panel); if (defaultAgent) { additionalPicks.push({ - label: localize('askXInChat', "Ask {0}: {1}", defaultAgent.metadata.fullName, filter), + label: localize('askXInChat', "Ask {0}: {1}", defaultAgent.fullName, filter), commandId: this.configuration.experimental.askChatLocation === 'quickChat' ? ASK_QUICK_QUESTION_ACTION_ID : CHAT_OPEN_ACTION_ID, args: [filter] }); diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index d062407d47a..260c84c94a2 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -214,7 +214,17 @@ declare module 'vscode' { */ export function createChatParticipant(id: string, handler: ChatExtendedRequestHandler): ChatParticipant; - export function createDynamicChatParticipant(id: string, name: string, publisherName: string, description: string, handler: ChatExtendedRequestHandler): ChatParticipant; + export function createDynamicChatParticipant(id: string, dynamicProps: DynamicChatParticipantProps, handler: ChatExtendedRequestHandler): ChatParticipant; + } + + /** + * These don't get set on the ChatParticipant after creation, like other props, because they are typically defined in package.json and we want them at the time of creation. + */ + export interface DynamicChatParticipantProps { + name: string; + publisherName: string; + description?: string; + fullName?: string; } /* diff --git a/src/vscode-dts/vscode.proposed.defaultChatParticipant.d.ts b/src/vscode-dts/vscode.proposed.defaultChatParticipant.d.ts index d600adb0fee..f4261b5b6fb 100644 --- a/src/vscode-dts/vscode.proposed.defaultChatParticipant.d.ts +++ b/src/vscode-dts/vscode.proposed.defaultChatParticipant.d.ts @@ -27,11 +27,6 @@ declare module 'vscode' { */ isDefault?: boolean; - /** - * The full name of this participant. - */ - fullName?: string; - /** * When true, this participant is invoked when the user submits their query using ctrl/cmd+enter * TODO@API name From a104ad1c7eaa528579017939c9e0bbd59576078e Mon Sep 17 00:00:00 2001 From: Aaron Munger Date: Thu, 9 May 2024 16:06:22 -0700 Subject: [PATCH 083/357] Set up save on EH for experiment roll-out (#212399) * enable experimental roll-out * no need to change backup delay --- .../contrib/notebook/browser/notebook.contribution.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 4be869da481..842b3915640 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -1041,9 +1041,10 @@ configurationRegistry.registerConfiguration({ tags: ['notebookLayout'] }, [NotebookSetting.remoteSaving]: { - markdownDescription: nls.localize('notebook.remoteSaving', "Enables the incremental saving of notebooks in Remote environment. When enabled, only the changes to the notebook are sent to the extension host, improving performance for large notebooks and slow network connections."), + markdownDescription: nls.localize('notebook.remoteSaving', "Enables the incremental saving of notebooks between processes and across Remote connections. When enabled, only the changes to the notebook are sent to the extension host, improving performance for large notebooks and slow network connections."), type: 'boolean', - default: typeof product.quality === 'string' && product.quality !== 'stable' // only enable as default in insiders + default: typeof product.quality === 'string' && product.quality !== 'stable', // only enable as default in insiders + tags: ['experimental'] }, [NotebookSetting.scrollToRevealCell]: { markdownDescription: nls.localize('notebook.scrolling.revealNextCellOnExecute.description', "How far to scroll when revealing the next cell upon running {0}.", 'notebook.cell.executeAndSelectBelow'), From 1e571b3c719ba9ce81538a96c96f6847c72ef5ad Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 9 May 2024 16:33:55 -0700 Subject: [PATCH 084/357] Avoid bad failure with out of date pre-release --- src/vs/workbench/api/browser/mainThreadChatAgents2.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index 385f5ac4e93..d8e616d224a 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -138,7 +138,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA disposable = this._chatAgentService.registerDynamicAgent( { id, - name: dynamicProps.name, + name: dynamicProps.name ?? '', // This case is for an API change and can be removed tomorrow description: dynamicProps.description, extensionId: extension, extensionDisplayName: extensionDescription?.displayName ?? extension.value, From 0f5d75143f8d1be1677a8c912ae6a51b712acb83 Mon Sep 17 00:00:00 2001 From: David Dossett Date: Thu, 9 May 2024 17:03:31 -0700 Subject: [PATCH 085/357] Fix used references padding --- src/vs/workbench/contrib/chat/browser/media/chat.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 6e3b0eeeef0..befebd0e43f 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -622,7 +622,7 @@ border: 1px solid var(--vscode-chat-requestBorder); border-radius: 4px; margin-bottom: 8px; - padding: 4px; + padding: 6px 8px; } .interactive-item-container .chat-notification-widget { From 3235435cf5df4df8dc7a4d22860b10fe39fecb80 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 9 May 2024 14:52:41 -0700 Subject: [PATCH 086/357] testing: implement initial UI for per-test coverage - There's now a toolbar on top of the file with test info for that file. This was inspired by some coverage extension I saw at one point: previously this was only shown in the explorer/test coverage view versus being contextual in the file - I really like the concept and utility of the toolbar, but it could certainly use a bit of polish. Maybe have it be sticky like breadcrumbs and styled more after notebooks' - I relocated the "toggle inline coverage" action from being the annoying popup on the line numbers into the toolbar - When per-test coverage is available, that's shown in the toolbar as well. Clicking on it allows you to filter to see only coverage generated by that test case. Per-test coverage filtering is global, and also applies in the Test Coverage view. - There's a pseudo-select box for filtering in the Test Coverage view (native select boxes are painful with a large number of items) - I think it's useful to show the code run by a test in the coverage view, but the numbers per-file are a little bogus, at least for the selfhost test provider, since I only show #'s for functions run by that test. Maybe we just don't show percentages in this mode. https://memes.peet.io/img/24-05-1941df72-bd93-42f9-9363-32fc3ea69e7d.mp4 --- .../api/browser/mainThreadTesting.ts | 2 +- src/vs/workbench/api/common/extHostTesting.ts | 1 + .../browser/codeCoverageDecorations.ts | 268 ++++++++++-------- .../browser/codeCoverageDisplayUtils.ts | 90 ++++++ .../contrib/testing/browser/media/testing.css | 78 +++++ .../testing/browser/testCoverageBars.ts | 81 ++---- .../testing/browser/testCoverageView.ts | 161 ++++++++++- .../contrib/testing/common/constants.ts | 5 +- .../contrib/testing/common/testCoverage.ts | 83 ++++-- .../testing/common/testCoverageService.ts | 19 +- .../contrib/testing/common/testId.ts | 21 ++ .../contrib/testing/common/testResult.ts | 5 + .../testing/common/testingContextKeys.ts | 1 + .../testing/test/common/testCoverage.test.ts | 8 +- 14 files changed, 608 insertions(+), 215 deletions(-) create mode 100644 src/vs/workbench/contrib/testing/browser/codeCoverageDisplayUtils.ts diff --git a/src/vs/workbench/api/browser/mainThreadTesting.ts b/src/vs/workbench/api/browser/mainThreadTesting.ts index a316bec0eb3..e6e48c56bcb 100644 --- a/src/vs/workbench/api/browser/mainThreadTesting.ts +++ b/src/vs/workbench/api/browser/mainThreadTesting.ts @@ -143,7 +143,7 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh transaction(tx => { let value = task.coverage.read(undefined); if (!value) { - value = new TestCoverage(taskId, this.uriIdentityService, { + value = new TestCoverage(run, taskId, this.uriIdentityService, { getCoverageDetails: (id, token) => this.proxy.$getCoverageDetails(id, token) .then(r => r.map(CoverageDetails.deserialize)), }); diff --git a/src/vs/workbench/api/common/extHostTesting.ts b/src/vs/workbench/api/common/extHostTesting.ts index 7865e9a2d3d..343cdc18143 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/src/vs/workbench/api/common/extHostTesting.ts @@ -565,6 +565,7 @@ class TestRunTracker extends Disposable { throw new Error('Attempted to `addCoverage` for a test item not included in the run'); } + this.ensureTestIsKnown(testItem); testItemIdPart = testItemCoverageId.get(testItem); if (testItemIdPart === undefined) { testItemIdPart = testItemCoverageId.size; diff --git a/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts b/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts index 8b310a5d71b..a6ce0ce1d1c 100644 --- a/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts +++ b/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts @@ -4,41 +4,44 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; -import { HoverWidget } from 'vs/base/browser/ui/hover/hoverWidget'; import { mapFindFirst } from 'vs/base/common/arraysFind'; -import { assertNever } from 'vs/base/common/assert'; +import { assert, assertNever } from 'vs/base/common/assert'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Lazy } from 'vs/base/common/lazy'; -import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { autorun, derived, observableFromEvent, observableValue } from 'vs/base/common/observable'; import { ThemeIcon } from 'vs/base/common/themables'; -import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, MouseTargetType } from 'vs/editor/browser/editorBrowser'; -import { MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; +import { ICodeEditor, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; -import { IModelDecorationOptions, ITextModel, InjectedTextCursorStops, InjectedTextOptions } from 'vs/editor/common/model'; -import { HoverOperation, HoverStartMode, IHoverComputer } from 'vs/editor/contrib/hover/browser/hoverOperation'; +import { IModelDecorationOptions, InjectedTextCursorStops, InjectedTextOptions, ITextModel } from 'vs/editor/common/model'; import { localize, localize2 } from 'vs/nls'; import { Categories } from 'vs/platform/action/common/actionCommonCategories'; import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ILogService } from 'vs/platform/log/common/log'; +import { IQuickInputService, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; +import * as coverUtils from 'vs/workbench/contrib/testing/browser/codeCoverageDisplayUtils'; import { testingCoverageMissingBranch } from 'vs/workbench/contrib/testing/browser/icons'; +import { ManagedTestCoverageBars } from 'vs/workbench/contrib/testing/browser/testCoverageBars'; +import { getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration'; import { FileCoverage } from 'vs/workbench/contrib/testing/common/testCoverage'; import { ITestCoverageService } from 'vs/workbench/contrib/testing/common/testCoverageService'; +import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { CoverageDetails, DetailType, IDeclarationCoverage, IStatementCoverage } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; const MAX_HOVERED_LINES = 30; const CLASS_HIT = 'coverage-deco-hit'; const CLASS_MISS = 'coverage-deco-miss'; -const TOGGLE_INLINE_COMMAND_TEXT = localize('testing.toggleInlineCoverage', 'Toggle Inline Coverage'); +const TOGGLE_INLINE_COMMAND_TEXT = localize('testing.toggleInlineCoverage', 'Toggle Inline'); const TOGGLE_INLINE_COMMAND_ID = 'testing.toggleInlineCoverage'; const BRANCH_MISS_INDICATOR_CHARS = 4; @@ -49,7 +52,7 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri private loadingCancellation?: CancellationTokenSource; private readonly displayedStore = this._register(new DisposableStore()); private readonly hoveredStore = this._register(new DisposableStore()); - private readonly lineHoverWidget: Lazy; + private readonly summaryWidget: Lazy; private decorationIds = new Map this._register(instantiationService.createInstance(LineHoverWidget, this.editor))); + this.summaryWidget = new Lazy(() => this._register(instantiationService.createInstance(CoverageSummaryWidget, this.editor))); const modelObs = observableFromEvent(editor.onDidChangeModel, () => editor.getModel()); const configObs = observableFromEvent(editor.onDidChangeConfiguration, i => i); @@ -82,8 +85,13 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri return; } - const file = report.getUri(model.uri); + let file = report.getUri(model.uri); if (file) { + const testFilter = coverage.filterToTest.read(reader); + if (testFilter) { + file = file.perTestData?.get(testFilter.toString()) || file; + } + return file; } @@ -114,8 +122,6 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri const model = editor.getModel(); if (e.target.type === MouseTargetType.GUTTER_LINE_NUMBERS && model) { this.hoverLineNumber(editor.getModel()!, e.target.position.lineNumber); - } else if (this.lineHoverWidget.hasValue && this.lineHoverWidget.value.getDomNode().contains(e.target.element)) { - // don't dismiss the hover } else if (CodeCoverageDecorations.showInline.get() && e.target.type === MouseTargetType.CONTENT_TEXT && model) { this.hoverInlineDecoration(model, e.target.position); } else { @@ -184,7 +190,6 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri const todo = [{ line: lineNumber, dir: 0 }]; const toEnable = new Set(); - const inlineEnabled = CodeCoverageDecorations.showInline.get(); if (!CodeCoverageDecorations.showInline.get()) { for (let i = 0; i < todo.length && i < MAX_HOVERED_LINES; i++) { const { line, dir } = todo[i]; @@ -215,16 +220,11 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri }); } - if (toEnable.size || inlineEnabled) { - this.lineHoverWidget.value.startShowingAt(lineNumber); - } - this.hoveredStore.add(this.editor.onMouseLeave(() => { this.hoveredStore.clear(); })); this.hoveredStore.add(toDisposable(() => { - this.lineHoverWidget.value.hide(); this.hoveredSubject = undefined; model.changeDecorations(e => { @@ -245,6 +245,7 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri } this.displayedStore.clear(); + this.summaryWidget.value.setCoverage(coverage); model.changeDecorations(e => { for (const detailRange of details.ranges) { @@ -308,6 +309,8 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri }); this.displayedStore.add(toDisposable(() => { + this.summaryWidget.value.setCoverage(undefined); + model.changeDecorations(e => { for (const decoration of this.decorationIds.keys()) { e.removeDecoration(decoration); @@ -499,27 +502,6 @@ function tidyLocation(location: Range | Position): Range { return location; } -class LineHoverComputer implements IHoverComputer { - public line = -1; - - constructor(@IKeybindingService private readonly keybindingService: IKeybindingService) { } - - /** @inheritdoc */ - public computeSync(): IMarkdownString[] { - const strs: IMarkdownString[] = []; - - const s = new MarkdownString().appendMarkdown(`[${TOGGLE_INLINE_COMMAND_TEXT}](command:${TOGGLE_INLINE_COMMAND_ID})`); - s.isTrusted = true; - const binding = this.keybindingService.lookupKeybinding(TOGGLE_INLINE_COMMAND_ID); - if (binding) { - s.appendText(` (${binding.getLabel()})`); - } - strs.push(s); - - return strs; - } -} - function wrapInBackticks(str: string) { return '`' + str.replace(/[\n\r`]/g, '') + '`'; } @@ -531,95 +513,155 @@ function wrapName(functionNameOrCode: string) { return wrapInBackticks(functionNameOrCode); } -class LineHoverWidget extends Disposable implements IOverlayWidget { - public static readonly ID = 'editor.contrib.testingCoverageLineHoverWidget'; +class CoverageSummaryWidget implements IDisposable { + private current: FileCoverage | undefined; + private registered = false; + private readonly registration = new DisposableStore(); - private readonly computer: LineHoverComputer; - private readonly hoverOperation: HoverOperation; - private readonly hover = this._register(new HoverWidget()); - private readonly renderDisposables = this._register(new DisposableStore()); - private readonly markdownRenderer: MarkdownRenderer; + private readonly _domNode = dom.h('div.coverage-summary-widget', [ + dom.h('div', [ + dom.h('span.bars@bars'), + dom.h('span.stat@stat'), + dom.h('a.toggleInline@toggleInline'), + dom.h('a.perTestFilter@perTestFilter'), + ]), + ]); - constructor(private readonly editor: ICodeEditor, @IInstantiationService instantiationService: IInstantiationService) { - super(); - this.computer = instantiationService.createInstance(LineHoverComputer); - this.markdownRenderer = this._register(instantiationService.createInstance(MarkdownRenderer, { editor: this.editor })); - this.hoverOperation = this._register(new HoverOperation(this.editor, this.computer)); - this.hover.containerDomNode.classList.add('hidden'); - this.hoverOperation.onResult(result => { - if (result.value.length) { - this.render(result.value); - } else { - this.hide(); - } + private readonly bars: ManagedTestCoverageBars; + + + constructor( + private readonly editor: ICodeEditor, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IQuickInputService private readonly quickInputService: IQuickInputService, + @ITestCoverageService private readonly testCoverageService: ITestCoverageService, + @IKeybindingService keybindingService: IKeybindingService, + @IInstantiationService instaService: IInstantiationService, + ) { + this._domNode.perTestFilter.ariaLabel = this._domNode.perTestFilter.title = coverUtils.labels.clickToChangeFiltering; + this.bars = instaService.createInstance(ManagedTestCoverageBars, { + compact: false, + overall: false, + container: this._domNode.bars, }); - this.editor.addOverlayWidget(this); + + const kb = keybindingService.lookupKeybinding(TOGGLE_INLINE_COMMAND_ID); + if (kb) { + this._domNode.toggleInline.title = `${TOGGLE_INLINE_COMMAND_TEXT} (${kb.getLabel()})`; + } + } + + public setCoverage(coverage: FileCoverage | undefined) { + this.current = coverage; + this.bars.setCoverageInfo(coverage); + + if (!coverage) { + return this.unregister(); + } + + const displayStat = coverUtils.calculateDisplayedStat(coverage, getTestingConfiguration(this.configurationService, TestingConfigKeys.CoveragePercent)); + this._domNode.stat.innerText = localize('testing.percentCoverage', '{0} Coverage', coverUtils.displayPercent(displayStat)); + + this._domNode.perTestFilter.classList.toggle('active', !!coverage.isForTest); + if (coverage.isForTest) { + const testItem = coverage.fromResult.getTestById(coverage.isForTest.id.toString()); + assert(!!testItem, 'got coverage for an unreported test'); + this._domNode.perTestFilter.style.display = 'inline'; + this._domNode.perTestFilter.innerText = coverUtils.labels.showingFilterFor(testItem.label); + } else if (coverage.perTestData?.size) { + this._domNode.perTestFilter.style.display = 'inline'; + this._domNode.perTestFilter.innerText = localize('testing.coverageForTestAvailable', "{0} test(s) in this file", coverage.perTestData.size); + } else { + this._domNode.perTestFilter.style.display = 'none'; + } + + this.register(); } /** @inheritdoc */ - getId(): string { - return LineHoverWidget.ID; + public dispose() { + this.unregister(); + this.bars.dispose(); } - /** @inheritdoc */ - public getDomNode(): HTMLElement { - return this.hover.containerDomNode; - } - - /** @inheritdoc */ - public getPosition(): IOverlayWidgetPosition | null { - return null; - } - - /** @inheritdoc */ - public override dispose(): void { - this.editor.removeOverlayWidget(this); - super.dispose(); - } - - /** Shows the hover widget at the given line */ - public startShowingAt(lineNumber: number) { - this.hide(); - const textModel = this.editor.getModel(); - if (!textModel) { + private filterTest() { + const options = this.current?.perTestData ?? this.current?.isForTest?.parent.perTestData; + if (!options) { return; } - this.computer.line = lineNumber; - this.hoverOperation.start(HoverStartMode.Delayed); + const tests = [...options.values()]; + const commonPrefix = TestId.getLengthOfCommonPrefix(tests.length, i => tests[i].isForTest!.id); + const result = this.current!.fromResult; + const previousSelection = this.testCoverageService.filterToTest.get(); + + type TItem = { label: string; description?: string; item: FileCoverage | undefined }; + + const items: QuickPickInput[] = [ + { label: coverUtils.labels.allTests, item: undefined }, + { type: 'separator' }, + ...tests.map(item => ({ label: coverUtils.getLabelForItem(result, item.isForTest!.id, commonPrefix), description: coverUtils.labels.percentCoverage(item.tpc), item })), + ]; + + this.quickInputService.pick(items, { + activeItem: items.find((item): item is TItem => 'item' in item && item.item === this.current), + placeHolder: coverUtils.labels.pickShowCoverage, + onDidFocus: (entry) => { + this.testCoverageService.filterToTest.set(entry.item?.isForTest!.id, undefined); + }, + }).then(selected => { + this.testCoverageService.filterToTest.set(selected ? selected.item?.isForTest!.id : previousSelection, undefined); + }); } - /** Hides the hover widget */ - public hide() { - this.hoverOperation.cancel(); - this.hover.containerDomNode.classList.add('hidden'); - } - - private render(elements: IMarkdownString[]) { - const { hover: h, editor: editor } = this; - const fragment = document.createDocumentFragment(); - - for (const msg of elements) { - const markdownHoverElement = dom.$('div.hover-row.markdown-hover'); - const hoverContentsElement = dom.append(markdownHoverElement, dom.$('div.hover-contents')); - const renderedContents = this.renderDisposables.add(this.markdownRenderer.render(msg)); - hoverContentsElement.appendChild(renderedContents.element); - fragment.appendChild(markdownHoverElement); + private register() { + if (this.registered) { + return; } - dom.clearNode(h.contentsDomNode); - h.contentsDomNode.appendChild(fragment); + this.registered = true; - h.containerDomNode.classList.remove('hidden'); - const editorLayout = editor.getLayoutInfo(); - const topForLineNumber = editor.getTopForLineNumber(this.computer.line); - const editorScrollTop = editor.getScrollTop(); - const lineHeight = editor.getOption(EditorOption.lineHeight); - const nodeHeight = h.containerDomNode.clientHeight; - const top = topForLineNumber - editorScrollTop - ((nodeHeight - lineHeight) / 2); - const left = editorLayout.lineNumbersLeft + editorLayout.lineNumbersWidth; - h.containerDomNode.style.left = `${left}px`; - h.containerDomNode.style.top = `${Math.max(Math.round(top), 0)}px`; + let viewZoneId: string; + this.editor.changeViewZones(accessor => { + viewZoneId = accessor.addZone({ + afterLineNumber: 0, + afterColumn: 0, + domNode: this._domNode.root, + heightInPx: 30, + ordinal: -1, // show before code lenses + }); + }); + + this.registration.add(toDisposable(() => { + this.editor.changeViewZones(accessor => { + accessor.removeZone(viewZoneId); + }); + this.registered = false; + })); + + this.registration.add(dom.addStandardDisposableListener(this._domNode.perTestFilter, 'click', () => { + this.filterTest(); + })); + + this.registration.add(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(TestingConfigKeys.CoverageBarThresholds) || e.affectsConfiguration(TestingConfigKeys.CoveragePercent)) { + this.setCoverage(this.current); + } + })); + + this.registration.add(dom.addStandardDisposableListener(this._domNode.toggleInline, 'click', () => { + CodeCoverageDecorations.showInline.set(!CodeCoverageDecorations.showInline.get(), undefined); + })); + + this.registration.add(autorun(reader => { + this._domNode.toggleInline.innerText = CodeCoverageDecorations.showInline.read(reader) + ? localize('testing.hideInlineCoverage', 'Hide Inline Coverage') + : localize('testing.showInlineCoverage', 'Show Inline Coverage'); + })); + } + + private unregister() { + this.registration.clear(); } } diff --git a/src/vs/workbench/contrib/testing/browser/codeCoverageDisplayUtils.ts b/src/vs/workbench/contrib/testing/browser/codeCoverageDisplayUtils.ts new file mode 100644 index 00000000000..d6a02334270 --- /dev/null +++ b/src/vs/workbench/contrib/testing/browser/codeCoverageDisplayUtils.ts @@ -0,0 +1,90 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { assertNever } from 'vs/base/common/assert'; +import { clamp } from 'vs/base/common/numbers'; +import { localize } from 'vs/nls'; +import { chartsGreen, chartsRed, chartsYellow } from 'vs/platform/theme/common/colorRegistry'; +import { asCssVariableName } from 'vs/platform/theme/common/colorUtils'; +import { CoverageBarSource } from 'vs/workbench/contrib/testing/browser/testCoverageBars'; +import { ITestingCoverageBarThresholds, TestingDisplayedCoveragePercent } from 'vs/workbench/contrib/testing/common/configuration'; +import { getTotalCoveragePercent } from 'vs/workbench/contrib/testing/common/testCoverage'; +import { TestId } from 'vs/workbench/contrib/testing/common/testId'; +import { LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult'; +import { ICoverageCount } from 'vs/workbench/contrib/testing/common/testTypes'; + +export const percent = (cc: ICoverageCount) => clamp(cc.total === 0 ? 1 : cc.covered / cc.total, 0, 1); + +const colorThresholds = [ + { color: `var(${asCssVariableName(chartsRed)})`, key: 'red' }, + { color: `var(${asCssVariableName(chartsYellow)})`, key: 'yellow' }, + { color: `var(${asCssVariableName(chartsGreen)})`, key: 'green' }, +] as const; + +export const getCoverageColor = (pct: number, thresholds: ITestingCoverageBarThresholds) => { + let best = colorThresholds[0].color; // red + let distance = pct; + for (const { key, color } of colorThresholds) { + const t = thresholds[key] / 100; + if (t && pct >= t && pct - t < distance) { + best = color; + distance = pct - t; + } + } + return best; +}; + + +const epsilon = 10e-8; + +export const displayPercent = (value: number, precision = 2) => { + const display = (value * 100).toFixed(precision); + + // avoid showing 100% coverage if it just rounds up: + if (value < 1 - epsilon && display === '100') { + return `${100 - (10 ** -precision)}%`; + } + + return `${display}%`; +}; + +export const calculateDisplayedStat = (coverage: CoverageBarSource, method: TestingDisplayedCoveragePercent) => { + switch (method) { + case TestingDisplayedCoveragePercent.Statement: + return percent(coverage.statement); + case TestingDisplayedCoveragePercent.Minimum: { + let value = percent(coverage.statement); + if (coverage.branch) { value = Math.min(value, percent(coverage.branch)); } + if (coverage.declaration) { value = Math.min(value, percent(coverage.declaration)); } + return value; + } + case TestingDisplayedCoveragePercent.TotalCoverage: + return getTotalCoveragePercent(coverage.statement, coverage.branch, coverage.declaration); + default: + assertNever(method); + } +}; + +export function getLabelForItem(result: LiveTestResult, testId: TestId, commonPrefixLen: number) { + const parts: string[] = []; + for (const id of testId.idsFromRoot()) { + const item = result.getTestById(id.toString()); + if (!item) { + break; + } + + parts.push(item.label); + } + + return parts.slice(commonPrefixLen).join(' \u203a '); +} + +export namespace labels { + export const showingFilterFor = (label: string) => localize('testing.coverageForTest', "Showing \"{0}\"", label); + export const clickToChangeFiltering = localize('changePerTestFilter', 'Click to view coverage for a single test'); + export const percentCoverage = (percent: number, precision?: number) => localize('testing.percentCoverage', '{0} Coverage', displayPercent(percent, precision)); + export const allTests = localize('testing.allTests', 'All tests'); + export const pickShowCoverage = localize('testing.pickTest', 'Pick a test to show coverage for'); +} diff --git a/src/vs/workbench/contrib/testing/browser/media/testing.css b/src/vs/workbench/contrib/testing/browser/media/testing.css index bd571b4a2df..4271a033ccc 100644 --- a/src/vs/workbench/contrib/testing/browser/media/testing.css +++ b/src/vs/workbench/contrib/testing/browser/media/testing.css @@ -401,6 +401,84 @@ opacity: 0.7; } +.coverage-summary-widget { + color: var(--vscode-editor-foreground); + z-index: 1; + line-height: 25px; + + > div { + display: flex; + align-items: center; + border-bottom: 1px solid var(--vscode-menu-border); + } + + .toggleInline, .perTestFilter { + border-left: 1px solid var(--vscode-menu-border); + padding: 0 6px; + } + + .stat, .toggleInline { + padding-right: 6px; + } + + > span, > a { + display: inline; + position: relative; + padding: 0 6px; + + &:first-child { + padding-left: 0; + } + + &:last-child { + padding-right: 0; + } + } + + + a { + color: var(--vscode-textLink-foreground); + cursor: pointer; + } + + a:hover { + color: var(--vscode-textLink-activeForeground); + } + + .toggleInline, .perTestFilter { + border-left: 1px solid var(--vscode-menu-border); + } +} + +.test-coverage-tree-per-test-switcher { + display: flex; + background-color: var(--vscode-dropdown-background); + color: var(--vscode-dropdown-foreground); + border: 1px solid var(--vscode-dropdown-border); + + margin: 3px 0; + cursor: pointer; + margin-right: 22px; + line-height: 20px; + padding: 0 6px; + width: fit-content; + max-width: calc(100% - 44px); + + span { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + &::after { + content: ''; + content: var(--vscode-icon-chevron-right-content); + font-family: var(--vscode-icon-chevron-right-font-family); + font-size: 18px; + padding-left: 22px; + } +} + /** -- coverage in the explorer */ .explorer-item-with-test-coverage { diff --git a/src/vs/workbench/contrib/testing/browser/testCoverageBars.ts b/src/vs/workbench/contrib/testing/browser/testCoverageBars.ts index 7a485f44be7..3e4cf9e7131 100644 --- a/src/vs/workbench/contrib/testing/browser/testCoverageBars.ts +++ b/src/vs/workbench/contrib/testing/browser/testCoverageBars.ts @@ -6,11 +6,9 @@ import { h } from 'vs/base/browser/dom'; import type { IUpdatableHover, IUpdatableHoverTooltipMarkdownString } from 'vs/base/browser/ui/hover/hover'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; -import { assertNever } from 'vs/base/common/assert'; import { MarkdownString } from 'vs/base/common/htmlContent'; import { Lazy } from 'vs/base/common/lazy'; import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; -import { clamp } from 'vs/base/common/numbers'; import { ITransaction, autorun, observableValue } from 'vs/base/common/observable'; import { isDefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; @@ -18,12 +16,12 @@ import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IHoverService } from 'vs/platform/hover/browser/hover'; import { Registry } from 'vs/platform/registry/common/platform'; -import { asCssVariableName, chartsGreen, chartsRed, chartsYellow } from 'vs/platform/theme/common/colorRegistry'; import { ExplorerExtensions, IExplorerFileContribution, IExplorerFileContributionRegistry } from 'vs/workbench/contrib/files/browser/explorerFileContrib'; -import { ITestingCoverageBarThresholds, TestingConfigKeys, TestingDisplayedCoveragePercent, getTestingConfiguration, observeTestingConfiguration } from 'vs/workbench/contrib/testing/common/configuration'; -import { AbstractFileCoverage, getTotalCoveragePercent } from 'vs/workbench/contrib/testing/common/testCoverage'; +import * as coverUtils from 'vs/workbench/contrib/testing/browser/codeCoverageDisplayUtils'; +import { calculateDisplayedStat } from 'vs/workbench/contrib/testing/browser/codeCoverageDisplayUtils'; +import { ITestingCoverageBarThresholds, TestingConfigKeys, getTestingConfiguration, observeTestingConfiguration } from 'vs/workbench/contrib/testing/common/configuration'; +import { AbstractFileCoverage } from 'vs/workbench/contrib/testing/common/testCoverage'; import { ITestCoverageService } from 'vs/workbench/contrib/testing/common/testCoverageService'; -import { ICoverageCount } from 'vs/workbench/contrib/testing/common/testTypes'; export interface TestCoverageBarsOptions { /** @@ -31,6 +29,10 @@ export interface TestCoverageBarsOptions { * overall bar is shown and more details are given in the hover. */ compact: boolean; + /** + * Whether the overall stat is shown, defaults to true. + */ + overall?: boolean; /** * Container in which is render the bars. */ @@ -120,19 +122,21 @@ export class ManagedTestCoverageBars extends Disposable { const precision = this.options.compact ? 0 : 2; const thresholds = getTestingConfiguration(this.configurationService, TestingConfigKeys.CoverageBarThresholds); const overallStat = calculateDisplayedStat(coverage, getTestingConfiguration(this.configurationService, TestingConfigKeys.CoveragePercent)); - el.overall.textContent = displayPercent(overallStat, precision); + if (this.options.overall !== false) { + el.overall.textContent = coverUtils.displayPercent(overallStat, precision); + } else { + el.overall.style.display = 'none'; + } if ('tpcBar' in el) { // compact mode renderBar(el.tpcBar, overallStat, false, thresholds); } else { - renderBar(el.statement, percent(coverage.statement), coverage.statement.total === 0, thresholds); - renderBar(el.function, coverage.declaration && percent(coverage.declaration), coverage.declaration?.total === 0, thresholds); - renderBar(el.branch, coverage.branch && percent(coverage.branch), coverage.branch?.total === 0, thresholds); + renderBar(el.statement, coverUtils.percent(coverage.statement), coverage.statement.total === 0, thresholds); + renderBar(el.function, coverage.declaration && coverUtils.percent(coverage.declaration), coverage.declaration?.total === 0, thresholds); + renderBar(el.branch, coverage.branch && coverUtils.percent(coverage.branch), coverage.branch?.total === 0, thresholds); } } } -const percent = (cc: ICoverageCount) => clamp(cc.total === 0 ? 1 : cc.covered / cc.total, 0, 1); -const epsilon = 10e-8; const barWidth = 16; const renderBar = (bar: HTMLElement, pct: number | undefined, isZero: boolean, thresholds: ITestingCoverageBarThresholds) => { @@ -152,59 +156,14 @@ const renderBar = (bar: HTMLElement, pct: number | undefined, isZero: boolean, t return; } - let best = colorThresholds[0].color; // red - let distance = pct; - for (const { key, color } of colorThresholds) { - const t = thresholds[key] / 100; - if (t && pct >= t && pct - t < distance) { - best = color; - distance = pct - t; - } - } - - bar.style.color = best; + bar.style.color = coverUtils.getCoverageColor(pct, thresholds); bar.style.opacity = '1'; }; -const colorThresholds = [ - { color: `var(${asCssVariableName(chartsRed)})`, key: 'red' }, - { color: `var(${asCssVariableName(chartsYellow)})`, key: 'yellow' }, - { color: `var(${asCssVariableName(chartsGreen)})`, key: 'green' }, -] as const; - -const calculateDisplayedStat = (coverage: CoverageBarSource, method: TestingDisplayedCoveragePercent) => { - switch (method) { - case TestingDisplayedCoveragePercent.Statement: - return percent(coverage.statement); - case TestingDisplayedCoveragePercent.Minimum: { - let value = percent(coverage.statement); - if (coverage.branch) { value = Math.min(value, percent(coverage.branch)); } - if (coverage.declaration) { value = Math.min(value, percent(coverage.declaration)); } - return value; - } - case TestingDisplayedCoveragePercent.TotalCoverage: - return getTotalCoveragePercent(coverage.statement, coverage.branch, coverage.declaration); - default: - assertNever(method); - } - -}; - -const displayPercent = (value: number, precision = 2) => { - const display = (value * 100).toFixed(precision); - - // avoid showing 100% coverage if it just rounds up: - if (value < 1 - epsilon && display === '100') { - return `${100 - (10 ** -precision)}%`; - } - - return `${display}%`; -}; - const nf = new Intl.NumberFormat(); -const stmtCoverageText = (coverage: CoverageBarSource) => localize('statementCoverage', '{0}/{1} statements covered ({2})', nf.format(coverage.statement.covered), nf.format(coverage.statement.total), displayPercent(percent(coverage.statement))); -const fnCoverageText = (coverage: CoverageBarSource) => coverage.declaration && localize('functionCoverage', '{0}/{1} functions covered ({2})', nf.format(coverage.declaration.covered), nf.format(coverage.declaration.total), displayPercent(percent(coverage.declaration))); -const branchCoverageText = (coverage: CoverageBarSource) => coverage.branch && localize('branchCoverage', '{0}/{1} branches covered ({2})', nf.format(coverage.branch.covered), nf.format(coverage.branch.total), displayPercent(percent(coverage.branch))); +const stmtCoverageText = (coverage: CoverageBarSource) => localize('statementCoverage', '{0}/{1} statements covered ({2})', nf.format(coverage.statement.covered), nf.format(coverage.statement.total), coverUtils.displayPercent(coverUtils.percent(coverage.statement))); +const fnCoverageText = (coverage: CoverageBarSource) => coverage.declaration && localize('functionCoverage', '{0}/{1} functions covered ({2})', nf.format(coverage.declaration.covered), nf.format(coverage.declaration.total), coverUtils.displayPercent(coverUtils.percent(coverage.declaration))); +const branchCoverageText = (coverage: CoverageBarSource) => coverage.branch && localize('branchCoverage', '{0}/{1} branches covered ({2})', nf.format(coverage.branch.covered), nf.format(coverage.branch.total), coverUtils.displayPercent(coverUtils.percent(coverage.branch))); const getOverallHoverText = (coverage: CoverageBarSource): IUpdatableHoverTooltipMarkdownString => { const str = [ diff --git a/src/vs/workbench/contrib/testing/browser/testCoverageView.ts b/src/vs/workbench/contrib/testing/browser/testCoverageView.ts index c4270ad9e2a..ff7bda28075 100644 --- a/src/vs/workbench/contrib/testing/browser/testCoverageView.ts +++ b/src/vs/workbench/contrib/testing/browser/testCoverageView.ts @@ -23,7 +23,8 @@ import { URI } from 'vs/base/common/uri'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { localize, localize2 } from 'vs/nls'; -import { MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; @@ -35,19 +36,22 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ILabelService } from 'vs/platform/label/common/label'; import { WorkbenchCompressibleObjectTree } from 'vs/platform/list/browser/listService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; import { IViewPaneOptions, ViewAction, ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { IViewDescriptorService } from 'vs/workbench/common/views'; +import * as coverUtils from 'vs/workbench/contrib/testing/browser/codeCoverageDisplayUtils'; import { testingStatesToIcons, testingWasCovered } from 'vs/workbench/contrib/testing/browser/icons'; import { CoverageBarSource, ManagedTestCoverageBars } from 'vs/workbench/contrib/testing/browser/testCoverageBars'; import { TestCommandId, Testing } from 'vs/workbench/contrib/testing/common/constants'; import { onObservableChange } from 'vs/workbench/contrib/testing/common/observableUtils'; import { ComputedFileCoverage, FileCoverage, TestCoverage, getTotalCoveragePercent } from 'vs/workbench/contrib/testing/common/testCoverage'; import { ITestCoverageService } from 'vs/workbench/contrib/testing/common/testCoverageService'; -import { CoverageDetails, DetailType, ICoverageCount, IDeclarationCoverage, TestResultState } from 'vs/workbench/contrib/testing/common/testTypes'; +import { TestId } from 'vs/workbench/contrib/testing/common/testId'; +import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; +import { CoverageDetails, DetailType, ICoverageCount, IDeclarationCoverage, ITestItem, TestResultState } from 'vs/workbench/contrib/testing/common/testTypes'; import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; const enum CoverageSortOrder { @@ -86,7 +90,7 @@ export class TestCoverageView extends ViewPane { const coverage = this.coverageService.selected.read(reader); if (coverage) { const t = (this.tree.value ??= this.instantiationService.createInstance(TestCoverageTree, container, labels, this.sortOrder)); - t.setInput(coverage); + t.setInput(coverage, this.coverageService.filterToTest.read(reader)); } else { this.tree.clear(); } @@ -191,9 +195,16 @@ class LoadingDetails { public readonly label = localize('loadingCoverageDetails', "Loading Coverage Details..."); } +class PerTestCoverageSwitcher { + public readonly id = String(fnNodeId++); + public readonly label = localize('changePerTestFilter', 'Click to change test filtering'); + + constructor(public readonly currentFilter: ITestItem | undefined) { } +} + /** Type of nodes returned from {@link TestCoverage}. Note: value is *always* defined. */ type TestCoverageFileNode = IPrefixTreeNode; -type CoverageTreeElement = TestCoverageFileNode | DeclarationCoverageNode | LoadingDetails | RevealUncoveredDeclarations; +type CoverageTreeElement = TestCoverageFileNode | DeclarationCoverageNode | LoadingDetails | RevealUncoveredDeclarations | PerTestCoverageSwitcher; const isFileCoverage = (c: CoverageTreeElement): c is TestCoverageFileNode => typeof c === 'object' && 'value' in c; const isDeclarationCoverage = (c: CoverageTreeElement): c is DeclarationCoverageNode => c instanceof DeclarationCoverageNode; @@ -222,6 +233,7 @@ class TestCoverageTree extends Disposable { instantiationService.createInstance(FileCoverageRenderer, labels), instantiationService.createInstance(DeclarationCoverageRenderer), instantiationService.createInstance(BasicRenderer), + instantiationService.createInstance(PerTestCoverageSwitcherRenderer), ], { expandOnlyOnTwistieClick: true, @@ -298,11 +310,22 @@ class TestCoverageTree extends Disposable { })); } - public setInput(coverage: TestCoverage) { + public setInput(coverage: TestCoverage, showOnlyTest?: TestId) { this.inputDisposables.clear(); - const files = []; - for (let node of coverage.tree.nodes) { + let tree = coverage.tree; + + // Filter to only a test, generate a new tree with only those items selected + if (showOnlyTest) { + tree = coverage.filterTreeForTest(showOnlyTest); + } + + const files: (PerTestCoverageSwitcher | TestCoverageFileNode)[] = []; + if (coverage.perTestCoverageIDs.size) { + files.push(new PerTestCoverageSwitcher(showOnlyTest ? coverage.result.getTestById(showOnlyTest.toString()) : undefined)); + } + + for (let node of tree.nodes) { // when showing initial children, only show from the first file or tee while (!(node.value instanceof FileCoverage) && node.children?.size === 1) { node = Iterable.first(node.children.values())!; @@ -310,15 +333,23 @@ class TestCoverageTree extends Disposable { files.push(node); } - const toChild = (file: TestCoverageFileNode): ICompressedTreeElement => { - const isFile = !file.children?.size; + const toChild = (value: TestCoverageFileNode | PerTestCoverageSwitcher): ICompressedTreeElement => { + if (value instanceof PerTestCoverageSwitcher) { + return { + element: value, + incompressible: true, + collapsible: false, + }; + } + + const isFile = !value.children?.size; return { - element: file, + element: value, incompressible: isFile, collapsed: isFile, // directories can be expanded, and items with function info can be expanded - collapsible: !isFile || !!file.value?.declaration?.total, - children: file.children && Iterable.map(file.children?.values(), toChild) + collapsible: !isFile || !!value.value?.declaration?.total, + children: value.children && Iterable.map(value.children?.values(), toChild) }; }; @@ -378,6 +409,10 @@ class TestCoverageTree extends Disposable { class TestCoverageTreeListDelegate implements IListVirtualDelegate { getHeight(element: CoverageTreeElement): number { + if (element instanceof PerTestCoverageSwitcher) { + return PerTestCoverageSwitcherRenderer.height; + } + return 22; } @@ -391,6 +426,9 @@ class TestCoverageTreeListDelegate implements IListVirtualDelegate { + public static readonly ID = 'S'; + public static readonly height = 28; + public readonly templateId = PerTestCoverageSwitcherRenderer.ID; + + constructor(@ICommandService private readonly commandService: ICommandService) { } + + renderCompressedElements(node: ITreeNode, FuzzyScore>, _index: number, data: PerTestCoverageSwitcherRendererTemplateData): void { + this.renderInner(node.element.elements[node.element.elements.length - 1], data); + } + + renderTemplate(container: HTMLElement): PerTestCoverageSwitcherRendererTemplateData { + const el = document.createElement('div'); + const text = document.createElement('span'); + el.classList.add('test-coverage-tree-per-test-switcher'); + el.appendChild(text); + container.appendChild(el); + + return { + container: el, + text, + elementDisposables: new DisposableStore(), + }; + } + + renderElement(node: ITreeNode, index: number, data: PerTestCoverageSwitcherRendererTemplateData): void { + this.renderInner(node.element, data); + } + + disposeTemplate(data: PerTestCoverageSwitcherRendererTemplateData): void { + data.elementDisposables.dispose(); + data.container.parentElement?.removeChild(data.container); + } + + private renderInner(element: PerTestCoverageSwitcher, { container, text, elementDisposables }: PerTestCoverageSwitcherRendererTemplateData) { + elementDisposables.clear(); + text.innerText = element.currentFilter + ? coverUtils.labels.showingFilterFor(element.currentFilter.label) + : localize('testing.filterCovToTest', 'Show coverage for test...'); + elementDisposables.add(dom.addStandardDisposableListener(container, 'click', evt => { + this.commandService.executeCommand(TestCommandId.CoverageFilterToTest, element.currentFilter?.extId); + evt.preventDefault(); + })); + } +} + class TestCoverageIdentityProvider implements IIdentityProvider { public getId(element: CoverageTreeElement) { return isFileCoverage(element) @@ -590,6 +681,50 @@ class TestCoverageIdentityProvider implements IIdentityProvider tests[i]); + const result = coverage.result; + const previousSelection = coverageService.filterToTest.get(); + const previousSelectionStr = previousSelection?.toString(); + + type TItem = { label: string; testId?: TestId }; + + const items: QuickPickInput[] = [ + { label: coverUtils.labels.allTests, id: undefined }, + { type: 'separator' }, + ...tests.map(testId => ({ label: coverUtils.getLabelForItem(result, testId, commonPrefix), testId })), + ]; + + quickInputService.pick(items, { + activeItem: items.find((item): item is TItem => 'testId' in item && item.testId?.toString() === previousSelectionStr), + placeHolder: coverUtils.labels.pickShowCoverage, + onDidFocus: (entry) => { + coverageService.filterToTest.set(entry.testId, undefined); + }, + }).then(selected => { + coverageService.filterToTest.set(selected ? selected.testId : previousSelection, undefined); + }); + } +}); + registerAction2(class TestCoverageChangeSortingAction extends ViewAction { constructor() { super({ diff --git a/src/vs/workbench/contrib/testing/common/constants.ts b/src/vs/workbench/contrib/testing/common/constants.ts index 8f6bb09e32a..2dfd8cf55c2 100644 --- a/src/vs/workbench/contrib/testing/common/constants.ts +++ b/src/vs/workbench/contrib/testing/common/constants.ts @@ -63,11 +63,12 @@ export const enum TestCommandId { ContinousRunUsingForTest = 'testing.continuousRunUsingForTest', CoverageAtCursor = 'testing.coverageAtCursor', CoverageByUri = 'testing.coverage.uri', - CoverageViewChangeSorting = 'testing.coverageViewChangeSorting', CoverageClear = 'testing.coverage.close', CoverageCurrentFile = 'testing.coverageCurrentFile', + CoverageFilterToTest = 'testing.coverageFilterToTest', CoverageLastRun = 'testing.coverageLastRun', CoverageSelectedAction = 'testing.coverageSelected', + CoverageViewChangeSorting = 'testing.coverageViewChangeSorting', DebugAction = 'testing.debug', DebugAllAction = 'testing.debugAll', DebugAtCursor = 'testing.debugAtCursor', @@ -81,8 +82,8 @@ export const enum TestCommandId { GetSelectedProfiles = 'testing.getSelectedProfiles', GoToTest = 'testing.editFocusedTest', HideTestAction = 'testing.hideTest', - OpenOutputPeek = 'testing.openOutputPeek', OpenCoverage = 'testing.openCoverage', + OpenOutputPeek = 'testing.openOutputPeek', RefreshTestsAction = 'testing.refreshTests', ReRunFailedTests = 'testing.reRunFailTests', ReRunLastRun = 'testing.reRunLastRun', diff --git a/src/vs/workbench/contrib/testing/common/testCoverage.ts b/src/vs/workbench/contrib/testing/common/testCoverage.ts index ada6359fded..37387047d93 100644 --- a/src/vs/workbench/contrib/testing/common/testCoverage.ts +++ b/src/vs/workbench/contrib/testing/common/testCoverage.ts @@ -11,6 +11,8 @@ import { ITransaction, observableSignal } from 'vs/base/common/observable'; import { IPrefixTreeNode, WellDefinedPrefixTree } from 'vs/base/common/prefixTree'; import { URI } from 'vs/base/common/uri'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { TestId } from 'vs/workbench/contrib/testing/common/testId'; +import { LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult'; import { CoverageDetails, ICoverageCount, IFileCoverage } from 'vs/workbench/contrib/testing/common/testTypes'; export interface ICoverageAccessor { @@ -26,10 +28,13 @@ export class TestCoverage { private readonly fileCoverage = new ResourceMap(); public readonly didAddCoverage = observableSignal[]>(this); public readonly tree = new WellDefinedPrefixTree(); - public readonly associatedData = new Map(); + /** Test IDs that have per-test coverage in this output. */ + public readonly perTestCoverageIDs = new Set(); + constructor( + public readonly result: LiveTestResult, public readonly fromTaskId: string, private readonly uriIdentityService: IUriIdentityService, private readonly accessor: ICoverageAccessor, @@ -37,6 +42,7 @@ export class TestCoverage { public append(coverage: IFileCoverage, tx: ITransaction | undefined) { const previous = this.getComputedForUri(coverage.uri); + const result = this.result; const applyDelta = (kind: 'statement' | 'branch' | 'declaration', node: ComputedFileCoverage) => { if (!node[kind]) { if (coverage[kind]) { @@ -54,16 +60,21 @@ export class TestCoverage { const canonical = [...this.treePathForUri(coverage.uri, /* canonical = */ true)]; const chain: IPrefixTreeNode[] = []; const isPerTestCoverage = !!coverage.testId; + if (coverage.testId) { + this.perTestCoverageIDs.add(coverage.testId.toString()); + } this.tree.mutatePath(this.treePathForUri(coverage.uri, /* canonical = */ false), node => { chain.push(node); if (chain.length === canonical.length) { // we reached our destination node, apply the coverage as necessary: if (isPerTestCoverage) { - const v = node.value ??= new FileCoverage(IFileCoverage.empty(String(incId++), coverage.uri), this.accessor); + const v = node.value ??= new FileCoverage(IFileCoverage.empty(String(incId++), coverage.uri), result, this.accessor); assert(v instanceof FileCoverage, 'coverage is unexpectedly computed'); v.perTestData ??= new Map(); - v.perTestData.set(coverage.testId!.toString(), new FileCoverage(coverage, this.accessor)); + const perTest = new FileCoverage(coverage, result, this.accessor); + perTest.isForTest = { id: coverage.testId!, parent: v }; + v.perTestData.set(coverage.testId!.toString(), perTest); this.fileCoverage.set(coverage.uri, v); } else if (node.value) { const v = node.value; @@ -72,10 +83,8 @@ export class TestCoverage { v.statement = coverage.statement; v.branch = coverage.branch; v.declaration = coverage.declaration; - v.existsInExtHost = true; } else { - const v = node.value = new FileCoverage(coverage, this.accessor); - v.existsInExtHost = true; + const v = node.value = new FileCoverage(coverage, result, this.accessor); this.fileCoverage.set(coverage.uri, v); } } else if (!isPerTestCoverage) { @@ -87,7 +96,7 @@ export class TestCoverage { const intermediate = deepClone(coverage); intermediate.id = String(incId++); intermediate.uri = this.treePathToUri(canonical.slice(0, chain.length)); - node.value = new ComputedFileCoverage(intermediate); + node.value = new ComputedFileCoverage(intermediate, result); } else { applyDelta('statement', node.value); applyDelta('branch', node.value); @@ -102,6 +111,48 @@ export class TestCoverage { } } + /** + * Builds a new tree filtered to per-test coverage data for the given ID. + */ + public filterTreeForTest(testId: TestId) { + const tree = new WellDefinedPrefixTree(); + for (const node of this.tree.values()) { + if (node instanceof FileCoverage) { + const fileData = node.perTestData?.get(testId.toString()); + if (!fileData) { + continue; + } + + const canonical = [...this.treePathForUri(fileData.uri, /* canonical = */ true)]; + const chain: IPrefixTreeNode[] = []; + tree.mutatePath(this.treePathForUri(fileData.uri, /* canonical = */ false), node => { + chain.push(node); + + if (chain.length === canonical.length) { + node.value = fileData; + } else { + node.value ??= new ComputedFileCoverage({ + id: String(incId++), + uri: this.treePathToUri(canonical.slice(0, chain.length)), + statement: { covered: 0, total: 0 }, + }, fileData.fromResult); + + for (const kind of ['statement', 'branch', 'declaration'] as const) { + const count = fileData[kind]; + if (count) { + const cc = (node.value[kind] ??= { covered: 0, total: 0 }); + cc.covered += count.covered; + cc.total += count.total; + } + } + } + }); + } + } + + return tree; + } + /** * Gets coverage information for all files. */ @@ -162,13 +213,6 @@ export abstract class AbstractFileCoverage { public declaration?: ICoverageCount; public readonly didChange = observableSignal(this); - /** - * Whether this coverage item exists in the extension host. This is false - * if we have only {@link perTestData} and not summary data for the file, or - * if the node is computed for a directory. - */ - public existsInExtHost = false; - /** * Gets the total coverage percent based on information provided. * This is based on the Clover total coverage formula @@ -177,7 +221,7 @@ export abstract class AbstractFileCoverage { return getTotalCoveragePercent(this.statement, this.branch, this.declaration); } - constructor(coverage: IFileCoverage) { + constructor(coverage: IFileCoverage, public readonly fromResult: LiveTestResult) { this.id = coverage.id; this.uri = coverage.uri; this.statement = coverage.statement; @@ -206,8 +250,13 @@ export class FileCoverage extends AbstractFileCoverage { */ public perTestData?: Map; - constructor(coverage: IFileCoverage, private readonly accessor: ICoverageAccessor) { - super(coverage); + /** + * If this is for a single test item, gets the test item. + */ + public isForTest?: { id: TestId; parent: FileCoverage }; + + constructor(coverage: IFileCoverage, fromResult: LiveTestResult, private readonly accessor: ICoverageAccessor) { + super(coverage, fromResult); } /** diff --git a/src/vs/workbench/contrib/testing/common/testCoverageService.ts b/src/vs/workbench/contrib/testing/common/testCoverageService.ts index 0bf62937458..1336f748f86 100644 --- a/src/vs/workbench/contrib/testing/common/testCoverageService.ts +++ b/src/vs/workbench/contrib/testing/common/testCoverageService.ts @@ -5,11 +5,12 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; -import { IObservable, observableValue } from 'vs/base/common/observable'; +import { IObservable, ISettableObservable, observableValue, transaction } from 'vs/base/common/observable'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Testing } from 'vs/workbench/contrib/testing/common/constants'; import { TestCoverage } from 'vs/workbench/contrib/testing/common/testCoverage'; +import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { ITestRunTaskResults } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; @@ -26,6 +27,11 @@ export interface ITestCoverageService { */ readonly selected: IObservable; + /** + * Filter to per-test coverage from the given test ID. + */ + readonly filterToTest: ISettableObservable; + /** * Opens a test coverage report from a task, optionally focusing it in the editor. */ @@ -40,9 +46,11 @@ export interface ITestCoverageService { export class TestCoverageService extends Disposable implements ITestCoverageService { declare readonly _serviceBrand: undefined; private readonly _isOpenKey: IContextKey; + private readonly _hasPerTestCoverage: IContextKey; private readonly lastOpenCts = this._register(new MutableDisposable()); public readonly selected = observableValue('testCoverage', undefined); + public readonly filterToTest = observableValue('filterToTest', undefined); constructor( @IContextKeyService contextKeyService: IContextKeyService, @@ -51,6 +59,7 @@ export class TestCoverageService extends Disposable implements ITestCoverageServ ) { super(); this._isOpenKey = TestingContextKeys.isTestCoverageOpen.bindTo(contextKeyService); + this._hasPerTestCoverage = TestingContextKeys.hasPerTestCoverage.bindTo(contextKeyService); this._register(resultService.onResultsChanged(evt => { if ('completed' in evt) { @@ -78,8 +87,13 @@ export class TestCoverageService extends Disposable implements ITestCoverageServ return; } - this.selected.set(coverage, undefined); + transaction(tx => { + // todo: may want to preserve this if coverage for that test in the new run? + this.filterToTest.set(undefined, tx); + this.selected.set(coverage, tx); + }); this._isOpenKey.set(true); + this._hasPerTestCoverage.set(coverage.perTestCoverageIDs.size > 0); if (focus && !cts.token.isCancellationRequested) { this.viewsService.openView(Testing.CoverageViewId, true); @@ -89,6 +103,7 @@ export class TestCoverageService extends Disposable implements ITestCoverageServ /** @inheritdoc */ public closeCoverage() { this._isOpenKey.set(false); + this._hasPerTestCoverage.set(false); this.selected.set(undefined, undefined); } } diff --git a/src/vs/workbench/contrib/testing/common/testId.ts b/src/vs/workbench/contrib/testing/common/testId.ts index 98bd5faec9b..79fcec77b1d 100644 --- a/src/vs/workbench/contrib/testing/common/testId.ts +++ b/src/vs/workbench/contrib/testing/common/testId.ts @@ -128,6 +128,27 @@ export class TestId { return TestPosition.Disconnected; } + public static getLengthOfCommonPrefix(length: number, getId: (i: number) => TestId): number { + if (length === 0) { + return 0; + } + + let commonPrefix = 0; + while (commonPrefix < length - 1) { + for (let i = 1; i < length; i++) { + const a = getId(i - 1); + const b = getId(i); + if (a.path[commonPrefix] !== b.path[commonPrefix]) { + return commonPrefix; + } + } + + commonPrefix++; + } + + return commonPrefix; + } + constructor( public readonly path: readonly string[], private readonly viewEnd = path.length, diff --git a/src/vs/workbench/contrib/testing/common/testResult.ts b/src/vs/workbench/contrib/testing/common/testResult.ts index 2fdd923c900..16b78d058cb 100644 --- a/src/vs/workbench/contrib/testing/common/testResult.ts +++ b/src/vs/workbench/contrib/testing/common/testResult.ts @@ -313,6 +313,11 @@ export class LiveTestResult extends Disposable implements ITestResult { return this.testById.values(); } + /** Gets an included test item by ID. */ + public getTestById(id: string) { + return this.testById.get(id)?.item; + } + private readonly computedStateAccessor: IComputedStateAccessor = { getOwnState: i => i.ownComputedState, getCurrentComputedState: i => i.computedState, diff --git a/src/vs/workbench/contrib/testing/common/testingContextKeys.ts b/src/vs/workbench/contrib/testing/common/testingContextKeys.ts index ddef4fcdc15..7878be0ec9e 100644 --- a/src/vs/workbench/contrib/testing/common/testingContextKeys.ts +++ b/src/vs/workbench/contrib/testing/common/testingContextKeys.ts @@ -22,6 +22,7 @@ export namespace TestingContextKeys { export const isParentRunningContinuously = new RawContextKey('testing.isParentRunningContinuously', false, { type: 'boolean', description: localize('testing.isParentRunningContinuously', 'Indicates whether the parent of a test is continuously running, set in the menu context of test items') }); export const activeEditorHasTests = new RawContextKey('testing.activeEditorHasTests', false, { type: 'boolean', description: localize('testing.activeEditorHasTests', 'Indicates whether any tests are present in the current editor') }); export const isTestCoverageOpen = new RawContextKey('testing.isTestCoverageOpen', false, { type: 'boolean', description: localize('testing.isTestCoverageOpen', 'Indicates whether a test coverage report is open') }); + export const hasPerTestCoverage = new RawContextKey('testing.hasPerTestCoverage', false, { type: 'boolean', description: localize('testing.hasPerTestCoverage', 'Indicates whether per-test coverage is available') }); export const capabilityToContextKey: { [K in TestRunProfileBitset]: RawContextKey } = { [TestRunProfileBitset.Run]: hasRunnableTests, diff --git a/src/vs/workbench/contrib/testing/test/common/testCoverage.test.ts b/src/vs/workbench/contrib/testing/test/common/testCoverage.test.ts index bad192a7c02..ea8bb1e5b6e 100644 --- a/src/vs/workbench/contrib/testing/test/common/testCoverage.test.ts +++ b/src/vs/workbench/contrib/testing/test/common/testCoverage.test.ts @@ -16,6 +16,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/uti import { onObservableChange } from 'vs/workbench/contrib/testing/common/observableUtils'; import { ICoverageAccessor, TestCoverage } from 'vs/workbench/contrib/testing/common/testCoverage'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; +import { LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult'; import { IFileCoverage } from 'vs/workbench/contrib/testing/common/testTypes'; suite('TestCoverage', () => { @@ -30,7 +31,7 @@ suite('TestCoverage', () => { coverageAccessor = { getCoverageDetails: sandbox.stub().resolves([]), }; - testCoverage = new TestCoverage('taskId', { extUri: { ignorePathCasing: () => true } } as any, coverageAccessor); + testCoverage = new TestCoverage({} as LiveTestResult, 'taskId', { extUri: { ignorePathCasing: () => true } } as any, coverageAccessor); }); teardown(() => { @@ -68,7 +69,6 @@ suite('TestCoverage', () => { assert.deepEqual(fileCoverage?.statement, raw1.statement); assert.deepEqual(fileCoverage?.branch, raw1.branch); assert.deepEqual(fileCoverage?.declaration, raw1.declaration); - assert.strictEqual(fileCoverage?.existsInExtHost, true); assert.strictEqual(testCoverage.getComputedForUri(raw1.uri), testCoverage.getUri(raw1.uri)); assert.strictEqual(testCoverage.getComputedForUri(URI.file('/path/to/x')), undefined); @@ -81,7 +81,6 @@ suite('TestCoverage', () => { assert.deepEqual(dirCoverage?.statement, { covered: 15, total: 30 }); assert.deepEqual(dirCoverage?.branch, { covered: 6, total: 15 }); assert.deepEqual(dirCoverage?.declaration, raw1.declaration); - assert.strictEqual(dirCoverage?.existsInExtHost, false); }); test('should incrementally diff updates to existing files', async () => { @@ -158,7 +157,6 @@ suite('TestCoverage', () => { // should be unchanged: assert.deepEqual(fileCoverage?.statement, { covered: 10, total: 20 }); - assert.deepEqual(fileCoverage?.existsInExtHost, true); const dirCoverage = testCoverage.getComputedForUri(URI.file('/path/to')); assert.deepEqual(dirCoverage?.statement, { covered: 15, total: 30 }); }); @@ -175,7 +173,6 @@ suite('TestCoverage', () => { testCoverage.append(raw3, undefined); const fileCoverage = testCoverage.getUri(raw3.uri); - assert.deepEqual(fileCoverage?.existsInExtHost, false); addTests(); @@ -187,7 +184,6 @@ suite('TestCoverage', () => { // should be the expected values: assert.deepEqual(fileCoverage?.statement, { covered: 10, total: 20 }); - assert.deepEqual(fileCoverage?.existsInExtHost, true); const dirCoverage = testCoverage.getComputedForUri(URI.file('/path/to')); assert.deepEqual(dirCoverage?.statement, { covered: 15, total: 30 }); }); From 1466eaeb6109811e5a74022a6391eca7022ecca9 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 9 May 2024 23:43:59 -0700 Subject: [PATCH 087/357] Attempt to partly fix layout issues with codeblocks (#212411) --- .../workbench/contrib/chat/browser/chatListRenderer.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index e4d158b307f..af062a7f95e 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -480,8 +480,11 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { + // Have to recompute the height here because codeblock rendering is currently async and it may have changed. + // If it becomes properly sync, then this could be removed. + element.currentRenderedHeight = templateData.rowContainer.offsetHeight; disposable.dispose(); - this._onDidChangeItemHeight.fire({ element, height: newHeight }); + this._onDidChangeItemHeight.fire({ element, height: element.currentRenderedHeight }); })); } } @@ -514,8 +517,11 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { + // Have to recompute the height here because codeblock rendering is currently async and it may have changed. + // If it becomes properly sync, then this could be removed. + element.currentRenderedHeight = templateData.rowContainer.offsetHeight; disposable.dispose(); - this._onDidChangeItemHeight.fire({ element, height: newHeight }); + this._onDidChangeItemHeight.fire({ element, height: element.currentRenderedHeight }); })); } } From 7d220c8ddaf50cc17610935777e4e5ebd3cc2a3e Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 10 May 2024 08:50:04 +0200 Subject: [PATCH 088/357] file watcher - skip flaky tests (win, linux) (#212416) --- .../platform/files/test/node/parcelWatcher.integrationTest.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts b/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts index 2f051861dd8..741b64d92c2 100644 --- a/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts +++ b/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts @@ -743,7 +743,7 @@ export class TestParcelWatcher extends ParcelWatcher { await testCorrelatedWatchFolderDoesNotExist(false); }); - test('correlated watch requests support suspend/resume (folder, does not exist in beginning, reusing watcher)', async () => { + (!isMacintosh /* Linux/Windows: times out for some reason */ ? test.skip : test)('correlated watch requests support suspend/resume (folder, does not exist in beginning, reusing watcher)', async () => { await testCorrelatedWatchFolderDoesNotExist(true); }); @@ -798,7 +798,7 @@ export class TestParcelWatcher extends ParcelWatcher { await testCorrelatedWatchFolderExists(false); }); - test('correlated watch requests support suspend/resume (folder, exist in beginning, reusing watcher)', async () => { + (!isMacintosh /* Linux/Windows: times out for some reason */ ? test.skip : test)('correlated watch requests support suspend/resume (folder, exist in beginning, reusing watcher)', async () => { await testCorrelatedWatchFolderExists(true); }); From 70e10d604e1939e9d98f3970f6f19604bfe2852c Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 10 May 2024 08:50:22 +0200 Subject: [PATCH 089/357] voice - offer to install speech from chat item (#212412) --- .../actions/voiceChatActions.ts | 68 +++++++++++++------ .../electron-sandbox/chat.contribution.ts | 5 +- 2 files changed, 52 insertions(+), 21 deletions(-) diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts index fcd736bca72..927f83dbd43 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts @@ -675,17 +675,35 @@ export class StartVoiceChatAction extends Action2 { const InstallingSpeechProvider = new RawContextKey('installingSpeechProvider', false, true); -export class InstallVoiceChatAction extends Action2 { - - static readonly ID = 'workbench.action.chat.installVoiceChat'; +abstract class BaseInstallSpeechProviderAction extends Action2 { private static readonly SPEECH_EXTENSION_ID = 'ms-vscode.vscode-speech'; + async run(accessor: ServicesAccessor): Promise { + const contextKeyService = accessor.get(IContextKeyService); + const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService); + try { + InstallingSpeechProvider.bindTo(contextKeyService).set(true); + await extensionsWorkbenchService.install(BaseInstallSpeechProviderAction.SPEECH_EXTENSION_ID, { + justification: this.getJustification(), + enable: true + }, ProgressLocation.Notification); + } finally { + InstallingSpeechProvider.bindTo(contextKeyService).set(false); + } + } + + protected abstract getJustification(): string; +} + +export class InstallSpeechProviderForVoiceChatAction extends BaseInstallSpeechProviderAction { + + static readonly ID = 'workbench.action.chat.installProviderForVoiceChat'; + constructor() { super({ - id: InstallVoiceChatAction.ID, - title: localize2('workbench.action.chat.startVoiceChat.label', "Start Voice Chat"), - category: CHAT_CATEGORY, + id: InstallSpeechProviderForVoiceChatAction.ID, + title: localize2('workbench.action.chat.installProviderForVoiceChat.label', "Start Voice Chat"), icon: Codicon.mic, precondition: InstallingSpeechProvider.negate(), menu: [{ @@ -702,18 +720,8 @@ export class InstallVoiceChatAction extends Action2 { }); } - async run(accessor: ServicesAccessor): Promise { - const contextKeyService = accessor.get(IContextKeyService); - const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService); - try { - InstallingSpeechProvider.bindTo(contextKeyService).set(true); - await extensionsWorkbenchService.install(InstallVoiceChatAction.SPEECH_EXTENSION_ID, { - justification: localize('confirmInstallDetail', "Microphone support requires this extension."), - enable: true - }, ProgressLocation.Notification); - } finally { - InstallingSpeechProvider.bindTo(contextKeyService).set(false); - } + protected getJustification(): string { + return localize('installProviderForVoiceChat.justification', "Microphone support requires this extension."); } } @@ -857,13 +865,35 @@ class ChatSynthesizerSessions { } } +export class InstallSpeechProviderForSynthesizeChatAction extends BaseInstallSpeechProviderAction { + + static readonly ID = 'workbench.action.chat.installProviderForSynthesis'; + + constructor() { + super({ + id: InstallSpeechProviderForSynthesizeChatAction.ID, + title: localize2('workbench.action.chat.installProviderForSynthesis.label', "Read Aloud"), + icon: Codicon.unmute, + precondition: InstallingSpeechProvider.negate(), + menu: [{ + id: MenuId.ChatMessageTitle, + when: HasSpeechProvider.negate(), + group: 'navigation' + }] + }); + } + + protected getJustification(): string { + return localize('installProviderForSynthesis.justification', "Speaker support requires this extension."); + } +} + export class ReadChatItemAloud extends Action2 { constructor() { super({ id: 'workbench.action.chat.readChatItemAloud', title: localize2('workbench.action.chat.readChatItemAloud', "Read Aloud"), f1: false, - category: CHAT_CATEGORY, icon: Codicon.unmute, precondition: CanVoiceChat, menu: { diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts b/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts index 45a4d8be6c7..682f22897cd 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts @@ -3,12 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { InlineVoiceChatAction, QuickVoiceChatAction, StartVoiceChatAction, StopListeningInQuickChatAction, StopListeningInChatEditorAction, StopListeningInChatViewAction, VoiceChatInChatViewAction, StopListeningAction, StopListeningAndSubmitAction, KeywordActivationContribution, InstallVoiceChatAction, StopListeningInTerminalChatAction, HoldToVoiceChatInChatViewAction, ReadChatItemAloud, StopReadAloud, StopReadChatItemAloud } from 'vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions'; +import { InlineVoiceChatAction, QuickVoiceChatAction, StartVoiceChatAction, StopListeningInQuickChatAction, StopListeningInChatEditorAction, StopListeningInChatViewAction, VoiceChatInChatViewAction, StopListeningAction, StopListeningAndSubmitAction, KeywordActivationContribution, InstallSpeechProviderForSynthesizeChatAction, InstallSpeechProviderForVoiceChatAction, StopListeningInTerminalChatAction, HoldToVoiceChatInChatViewAction, ReadChatItemAloud, StopReadAloud, StopReadChatItemAloud } from 'vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions'; import { registerAction2 } from 'vs/platform/actions/common/actions'; import { WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; registerAction2(StartVoiceChatAction); -registerAction2(InstallVoiceChatAction); +registerAction2(InstallSpeechProviderForVoiceChatAction); registerAction2(VoiceChatInChatViewAction); registerAction2(HoldToVoiceChatInChatViewAction); @@ -26,5 +26,6 @@ registerAction2(StopListeningInTerminalChatAction); registerAction2(ReadChatItemAloud); registerAction2(StopReadChatItemAloud); registerAction2(StopReadAloud); +registerAction2(InstallSpeechProviderForSynthesizeChatAction); registerWorkbenchContribution2(KeywordActivationContribution.ID, KeywordActivationContribution, WorkbenchPhase.AfterRestored); From aef6b4c700826a7541652b6921643361d31c1d63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Fri, 10 May 2024 15:19:34 +0200 Subject: [PATCH 090/357] quick pick: a11y workaround (#212248) * quick pick: a11y workaround fixes #211976 * fix tests --- .../quickinput/browser/quickInputTree.ts | 18 +++++++++++++++++- .../quickinput/test/browser/quickinput.test.ts | 3 +++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/vs/platform/quickinput/browser/quickInputTree.ts b/src/vs/platform/quickinput/browser/quickInputTree.ts index 80e68364349..3a492f78a1e 100644 --- a/src/vs/platform/quickinput/browser/quickInputTree.ts +++ b/src/vs/platform/quickinput/browser/quickInputTree.ts @@ -38,6 +38,7 @@ import { ThrottledDelayer } from 'vs/base/common/async'; import { isCancellationError } from 'vs/base/common/errors'; import type { IHoverWidget, IUpdatableHoverTooltipMarkdownString } from 'vs/base/browser/ui/hover/hover'; import { QuickPickFocus } from '../common/quickInput'; +import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; const $ = dom.$; @@ -706,7 +707,8 @@ export class QuickInputTree extends Disposable { private hoverDelegate: IHoverDelegate, private linkOpenerDelegate: (content: string) => void, id: string, - @IInstantiationService instantiationService: IInstantiationService + @IInstantiationService instantiationService: IInstantiationService, + @IAccessibilityService private readonly accessibilityService: IAccessibilityService ) { super(); this._container = dom.append(this.parent, $('.quick-input-list')); @@ -1114,6 +1116,20 @@ export class QuickInputTree extends Disposable { } this._tree.setChildren(null, elements); this._onChangedVisibleCount.fire(visibleCount); + + // Accessibility hack, unfortunately on next tick + // https://github.com/microsoft/vscode/issues/211976 + if (this.accessibilityService.isScreenReaderOptimized()) { + setTimeout(() => { + const focusedElement = this._tree.getHTMLElement().querySelector(`.monaco-list-row.focused`); + const parent = focusedElement?.parentNode; + if (focusedElement && parent) { + const nextSibling = focusedElement.nextSibling; + parent.removeChild(focusedElement); + parent.insertBefore(focusedElement, nextSibling); + } + }, 0); + } } getElementsCount(): number { diff --git a/src/vs/platform/quickinput/test/browser/quickinput.test.ts b/src/vs/platform/quickinput/test/browser/quickinput.test.ts index 216aa3a7f54..8e8fc694be0 100644 --- a/src/vs/platform/quickinput/test/browser/quickinput.test.ts +++ b/src/vs/platform/quickinput/test/browser/quickinput.test.ts @@ -32,6 +32,8 @@ import { ContextKeyService } from 'vs/platform/contextkey/browser/contextKeyServ import { NoMatchingKb } from 'vs/platform/keybinding/common/keybindingResolver'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ContextViewService } from 'vs/platform/contextview/browser/contextViewService'; +import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; +import { TestAccessibilityService } from 'vs/platform/accessibility/test/common/testAccessibilityService'; // Sets up an `onShow` listener to allow us to wait until the quick pick is shown (useful when triggering an `accept()` right after launching a quick pick) // kick this off before you launch the picker and then await the promise returned after you launch the picker. @@ -62,6 +64,7 @@ suite('QuickInput', () => { // https://github.com/microsoft/vscode/issues/147543 // Stub the services the quick input controller needs to function instantiationService.stub(IThemeService, new TestThemeService()); instantiationService.stub(IConfigurationService, new TestConfigurationService()); + instantiationService.stub(IAccessibilityService, new TestAccessibilityService()); instantiationService.stub(IListService, store.add(new ListService())); instantiationService.stub(ILayoutService, { activeContainer: fixture, onDidLayoutContainer: Event.None } as any); instantiationService.stub(IContextViewService, store.add(instantiationService.createInstance(ContextViewService))); From 4ec594faa273737fd70bb804a5a9b66ef400b8aa Mon Sep 17 00:00:00 2001 From: Ulugbek Abdullaev Date: Fri, 10 May 2024 16:37:05 +0200 Subject: [PATCH 091/357] runCommands: fix: `safeStringify` throws `RangeError: Invalid string length` for too big objects --- .../contrib/commands/common/commands.contribution.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/commands/common/commands.contribution.ts b/src/vs/workbench/contrib/commands/common/commands.contribution.ts index 5e5b329cd8e..55cf296c7f2 100644 --- a/src/vs/workbench/contrib/commands/common/commands.contribution.ts +++ b/src/vs/workbench/contrib/commands/common/commands.contribution.ts @@ -102,9 +102,9 @@ class RunCommands extends Action2 { logService.debug(`runCommands: executing ${i}-th command: ${safeStringify(cmd)}`); - const r = await this._runCommand(commandService, cmd); + await this._runCommand(commandService, cmd); - logService.debug(`runCommands: executed ${i}-th command with return value: ${safeStringify(r)}`); + logService.debug(`runCommands: executed ${i}-th command`); } } catch (err) { logService.debug(`runCommands: executing ${i}-th command resulted in an error: ${err instanceof Error ? err.message : safeStringify(err)}`); From c5e287efeacf4b83939b7248ad4dcb22ac7247d0 Mon Sep 17 00:00:00 2001 From: Aaron Munger Date: Fri, 10 May 2024 08:59:31 -0700 Subject: [PATCH 092/357] add telemetry to give insight for experiment (#212401) * add telemetry to give insight for experiment * conventional event name, skip for cancellation --- .../notebook/common/notebookEditorModel.ts | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index d96ad9d32a5..f2e90bd136d 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -15,6 +15,7 @@ import { assertType } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWriteFileOptions, IFileStatWithMetadata } from 'vs/platform/files/common/files'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IRevertOptions, ISaveOptions, IUntypedEditorInput } from 'vs/workbench/common/editor'; import { EditorModel } from 'vs/workbench/common/editor/editorModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; @@ -241,8 +242,30 @@ export class NotebookFileWorkingCopyModel extends Disposable implements IStoredF throw new CancellationError(); } - const stat = await serializer.save(this._notebookModel.uri, this._notebookModel.versionId, options, token); - return stat; + try { + const stat = await serializer.save(this._notebookModel.uri, this._notebookModel.versionId, options, token); + return stat; + } catch (error) { + if (!token.isCancellationRequested) { + type notebookSaveErrorData = { + isRemote: boolean; + versionMismatch: boolean; + }; + type notebookSaveErrorClassification = { + owner: 'amunger'; + comment: 'Detect if we are having issues saving a notebook on the Extension Host'; + isRemote: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Whether the save is happening on a remote file system' }; + versionMismatch: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'If the error was because of a version mismatch' }; + }; + const telemetry = {} as ITelemetryService; + telemetry.publicLogError2('notebook/SaveError', { + isRemote: this._notebookModel.uri.scheme === Schemas.vscodeRemote, + versionMismatch: error instanceof Error && error.message === 'Document version mismatch' + }); + } + + throw error; + } }; } From 5216c044283ba1f3c66caa092fd75ba0b3e3e5ca Mon Sep 17 00:00:00 2001 From: Robo Date: Sat, 11 May 2024 01:20:28 +0900 Subject: [PATCH 093/357] chore: update to electron 29 (#209818) * chore: update electron@29.1.0 * chore: update typings to 20.x * chore: bump electron@29.1.5 * ci: fix crash in compiling extensions-ci * chore: disable .d.ts check for build/ $ ../node_modules/.bin/tsc -p tsconfig.build.json node_modules/@types/chokidar/index.d.ts:21:14 - error TS2420: Class 'import("/Users/demohan/github/vscode/build/node_modules/@types/chokidar/index").FSWatcher' incorrectly implements interface 'import("fs").FSWatcher'. Type 'FSWatcher' is missing the following properties from type 'FSWatcher': ref, unref 21 export class FSWatcher extends EventEmitter implements fs.FSWatcher { ~~~~~~~~~ node_modules/chokidar/types/index.d.ts:8:14 - error TS2420: Class 'import("/Users/demohan/github/vscode/build/node_modules/chokidar/types/index").FSWatcher' incorrectly implements interface 'import("fs").FSWatcher'. Type 'FSWatcher' is missing the following properties from type 'FSWatcher': ref, unref 8 export class FSWatcher extends EventEmitter implements fs.FSWatcher { ~~~~~~~~~ Found 2 errors in 2 files. Errors Files 1 node_modules/@types/chokidar/index.d.ts:21 1 node_modules/chokidar/types/index.d.ts:8 Refs https://github.com/paulmillr/chokidar/commit/a0f9e09f64ce7ae70cc6ae1f5615f65cb35f532f * chore: update core types * temp: fix layer validation * chore: update nodejs checksums * ci: use latest v20 LTS for missing node-gyp Refs https://github.com/npm/cli/commit/eacec5f49060d3dfcdc3c7043115619e4bb22864 * ci: define LIBCPP_HARDENING_MODE * ci: fix crash in vscode-web-min-ci * chore: update rpm deps-list * chore: bump tree-sitter-typescript@0.20.5 * chore: bump electron@29.3.0 * chore: bump electron@29.3.1 * chore: update rpm deps-list for x86_64 * ci: disable io_uring UV backend on linux * ci: disable io_uring backend for oss as well * chore: update typings to 20.x * ci: add TODO for io_uring workaround * chore: bump distro * chore: update preinstall node version checks * chore: update @types/gulp Refs https://github.com/microsoft/vscode/issues/212442 * ci: disable io_uring in more test suites --- .nvmrc | 2 +- .../package.json | 2 +- .../vscode-selfhost-test-provider/yarn.lock | 8 +- .yarnrc | 4 +- .../darwin/product-build-darwin.yml | 3 +- .../linux/product-build-linux-test.yml | 36 +++++ build/azure-pipelines/linux/setup-env.sh | 10 +- build/azure-pipelines/product-compile.yml | 2 + .../azure-pipelines/web/product-build-web.yml | 1 + .../win32/product-build-win32.yml | 10 +- build/checksums/electron.txt | 150 +++++++++--------- build/checksums/nodejs.txt | 13 +- build/lib/layersChecker.js | 64 +++++++- build/lib/layersChecker.ts | 72 ++++++++- build/linux/dependencies-generator.js | 2 +- build/linux/dependencies-generator.ts | 2 +- build/linux/rpm/dep-lists.js | 6 + build/linux/rpm/dep-lists.ts | 6 + build/npm/preinstall.js | 7 +- build/package.json | 4 +- build/tsconfig.build.json | 3 +- build/yarn.lock | 136 ++++++++++------ cgmanifest.json | 14 +- extensions/configuration-editing/package.json | 2 +- extensions/configuration-editing/yarn.lock | 15 +- extensions/css-language-features/package.json | 2 +- .../css-language-features/server/package.json | 2 +- .../css-language-features/server/yarn.lock | 8 +- extensions/css-language-features/yarn.lock | 15 +- extensions/debug-auto-launch/package.json | 2 +- extensions/debug-auto-launch/yarn.lock | 15 +- extensions/debug-server-ready/package.json | 2 +- extensions/debug-server-ready/yarn.lock | 15 +- extensions/emmet/package.json | 2 +- extensions/emmet/yarn.lock | 15 +- extensions/extension-editing/package.json | 2 +- .../extension-editing/src/extensionLinter.ts | 2 +- extensions/extension-editing/yarn.lock | 15 +- extensions/git-base/package.json | 2 +- extensions/git-base/yarn.lock | 15 +- extensions/git/package.json | 2 +- extensions/git/src/util.ts | 2 +- extensions/git/yarn.lock | 15 +- extensions/github-authentication/package.json | 2 +- extensions/github-authentication/yarn.lock | 15 +- extensions/github/package.json | 2 +- extensions/github/yarn.lock | 15 +- extensions/grunt/package.json | 2 +- extensions/grunt/yarn.lock | 15 +- extensions/gulp/package.json | 2 +- extensions/gulp/yarn.lock | 15 +- .../html-language-features/package.json | 2 +- .../server/package.json | 2 +- .../server/src/languageModelCache.ts | 2 +- .../html-language-features/server/yarn.lock | 15 +- extensions/html-language-features/yarn.lock | 15 +- extensions/jake/package.json | 2 +- extensions/jake/yarn.lock | 15 +- .../json-language-features/package.json | 2 +- .../server/package.json | 2 +- .../server/src/languageModelCache.ts | 4 +- .../json-language-features/server/yarn.lock | 15 +- extensions/json-language-features/yarn.lock | 15 +- .../server/package.json | 2 +- .../server/yarn.lock | 15 +- extensions/merge-conflict/package.json | 2 +- extensions/merge-conflict/yarn.lock | 15 +- .../microsoft-authentication/package.json | 2 +- extensions/microsoft-authentication/yarn.lock | 15 +- extensions/npm/package.json | 2 +- extensions/npm/yarn.lock | 15 +- extensions/php-language-features/package.json | 2 +- extensions/php-language-features/yarn.lock | 15 +- extensions/references-view/package.json | 2 +- extensions/references-view/yarn.lock | 15 +- extensions/tunnel-forwarding/package.json | 2 +- extensions/tunnel-forwarding/yarn.lock | 15 +- .../typescript-language-features/package.json | 2 +- .../src/languageFeatures/diagnostics.ts | 2 +- .../src/languageFeatures/tagClosing.ts | 2 +- .../src/ui/typingsStatus.ts | 2 +- .../typescript-language-features/yarn.lock | 15 +- extensions/vscode-api-tests/package.json | 2 +- extensions/vscode-api-tests/src/memfs.ts | 2 +- extensions/vscode-api-tests/yarn.lock | 15 +- extensions/vscode-colorize-tests/package.json | 2 +- extensions/vscode-colorize-tests/yarn.lock | 15 +- extensions/vscode-test-resolver/package.json | 2 +- extensions/vscode-test-resolver/yarn.lock | 15 +- package.json | 6 +- remote/.yarnrc | 4 +- src/vs/base/node/shell.ts | 2 +- .../platform/terminal/node/terminalProcess.ts | 2 +- .../node/remoteExtensionHostAgentServer.ts | 2 +- test/automation/package.json | 2 +- test/automation/yarn.lock | 15 +- test/integration/browser/package.json | 2 +- test/integration/browser/yarn.lock | 15 +- test/smoke/package.json | 2 +- test/smoke/yarn.lock | 15 +- yarn.lock | 30 ++-- 101 files changed, 797 insertions(+), 361 deletions(-) diff --git a/.nvmrc b/.nvmrc index a9d087399d7..bc78e9f2695 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -18.19.0 +20.12.1 diff --git a/.vscode/extensions/vscode-selfhost-test-provider/package.json b/.vscode/extensions/vscode-selfhost-test-provider/package.json index 852c6c29af2..3b019f34ec9 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/package.json +++ b/.vscode/extensions/vscode-selfhost-test-provider/package.json @@ -71,7 +71,7 @@ }, "devDependencies": { "@types/mocha": "^10.0.6", - "@types/node": "18.x" + "@types/node": "20.x" }, "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", diff --git a/.vscode/extensions/vscode-selfhost-test-provider/yarn.lock b/.vscode/extensions/vscode-selfhost-test-provider/yarn.lock index 1117c1920dd..50478f52c73 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/yarn.lock +++ b/.vscode/extensions/vscode-selfhost-test-provider/yarn.lock @@ -30,10 +30,10 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-10.0.6.tgz#818551d39113081048bdddbef96701b4e8bb9d1b" integrity sha512-dJvrYWxP/UcXm36Qn36fxhUKu8A/xMRXVT2cliFF1Z7UA9liG5Psj3ezNSZw+5puH2czDXRLcXQxf8JbJt0ejg== -"@types/node@18.x": - version "18.19.26" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.26.tgz#18991279d0a0e53675285e8cf4a0823766349729" - integrity sha512-+wiMJsIwLOYCvUqSdKTrfkS8mpTp+MPINe6+Np4TAGFWWRWiBQ5kSq9nZGCSPkzx9mvT+uEukzpX4MOSCydcvw== +"@types/node@20.x": + version "20.12.11" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.11.tgz#c4ef00d3507000d17690643278a60dc55a9dc9be" + integrity sha512-vDg9PZ/zi+Nqp6boSOT7plNuthRugEKixDv5sFTIpkE89MmNtEArAShI4mxuX2+UrLEe9pxC1vm2cjm9YlWbJw== dependencies: undici-types "~5.26.4" diff --git a/.yarnrc b/.yarnrc index 616968bddff..31ceae81a48 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1,5 +1,5 @@ disturl "https://electronjs.org/headers" -target "28.2.8" -ms_build_id "27744544" +target "29.3.1" +ms_build_id "9464424" runtime "electron" build_from_source "true" diff --git a/build/azure-pipelines/darwin/product-build-darwin.yml b/build/azure-pipelines/darwin/product-build-darwin.yml index 11aa7605f63..8b4bda1c6a2 100644 --- a/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/build/azure-pipelines/darwin/product-build-darwin.yml @@ -78,8 +78,6 @@ steps: - script: | set -e - export npm_config_arch=$(VSCODE_ARCH) - npm i -g node-gyp@9.4.0 python3 -m pip install setuptools for i in {1..5}; do # try 5 times @@ -91,6 +89,7 @@ steps: echo "Yarn failed $i, trying again..." done env: + npm_config_arch: $(VSCODE_ARCH) ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 GITHUB_TOKEN: "$(github-distro-mixin-password)" diff --git a/build/azure-pipelines/linux/product-build-linux-test.yml b/build/azure-pipelines/linux/product-build-linux-test.yml index f5c00aa0cf0..4e225757a81 100644 --- a/build/azure-pipelines/linux/product-build-linux-test.yml +++ b/build/azure-pipelines/linux/product-build-linux-test.yml @@ -92,6 +92,9 @@ steps: - script: ./scripts/test-integration.sh --tfs "Integration Tests" env: DISPLAY: ":10" + # TODO(deepak1556): Remove this once runtime is updated for + # https://github.com/microsoft/vscode/issues/210467#issuecomment-2104566724 + UV_USE_IO_URING: 0 displayName: Run integration tests (Electron) timeoutInMinutes: 20 @@ -100,6 +103,10 @@ steps: timeoutInMinutes: 20 - script: ./scripts/test-remote-integration.sh + env: + # TODO(deepak1556): Remove this once runtime is updated for + # https://github.com/microsoft/vscode/issues/210467#issuecomment-2104566724 + UV_USE_IO_URING: 0 displayName: Run integration tests (Remote) timeoutInMinutes: 20 @@ -116,6 +123,9 @@ steps: ./scripts/test-integration.sh --build --tfs "Integration Tests" env: VSCODE_REMOTE_SERVER_PATH: $(agent.builddirectory)/vscode-server-linux-$(VSCODE_ARCH) + # TODO(deepak1556): Remove this once runtime is updated for + # https://github.com/microsoft/vscode/issues/210467#issuecomment-2104566724 + UV_USE_IO_URING: 0 displayName: Run integration tests (Electron) timeoutInMinutes: 20 @@ -134,6 +144,9 @@ steps: ./scripts/test-remote-integration.sh env: VSCODE_REMOTE_SERVER_PATH: $(agent.builddirectory)/vscode-server-linux-$(VSCODE_ARCH) + # TODO(deepak1556): Remove this once runtime is updated for + # https://github.com/microsoft/vscode/issues/210467#issuecomment-2104566724 + UV_USE_IO_URING: 0 displayName: Run integration tests (Remote) timeoutInMinutes: 20 @@ -160,24 +173,43 @@ steps: - script: yarn smoketest-no-compile --tracing timeoutInMinutes: 20 + env: + # TODO(deepak1556): Remove this once runtime is updated for + # https://github.com/microsoft/vscode/issues/210467#issuecomment-2104566724 + UV_USE_IO_URING: 0 displayName: Run smoke tests (Electron) - script: yarn smoketest-no-compile --web --tracing --headless --electronArgs="--disable-dev-shm-usage" timeoutInMinutes: 20 + env: + # TODO(deepak1556): Remove this once runtime is updated for + # https://github.com/microsoft/vscode/issues/210467#issuecomment-2104566724 + UV_USE_IO_URING: 0 displayName: Run smoke tests (Browser, Chromium) - script: yarn smoketest-no-compile --remote --tracing timeoutInMinutes: 20 + env: + # TODO(deepak1556): Remove this once runtime is updated for + # https://github.com/microsoft/vscode/issues/210467#issuecomment-2104566724 + UV_USE_IO_URING: 0 displayName: Run smoke tests (Remote) - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: - script: yarn smoketest-no-compile --tracing --build "$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH)" timeoutInMinutes: 20 + env: + # TODO(deepak1556): Remove this once runtime is updated for + # https://github.com/microsoft/vscode/issues/210467#issuecomment-2104566724 + UV_USE_IO_URING: 0 displayName: Run smoke tests (Electron) - script: yarn smoketest-no-compile --web --tracing --headless --electronArgs="--disable-dev-shm-usage" env: VSCODE_REMOTE_SERVER_PATH: $(agent.builddirectory)/vscode-server-linux-$(VSCODE_ARCH)-web + # TODO(deepak1556): Remove this once runtime is updated for + # https://github.com/microsoft/vscode/issues/210467#issuecomment-2104566724 + UV_USE_IO_URING: 0 timeoutInMinutes: 20 displayName: Run smoke tests (Browser, Chromium) @@ -188,6 +220,10 @@ steps: VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-server-linux-$(VSCODE_ARCH)" \ yarn smoketest-no-compile --tracing --remote --build "$APP_PATH" timeoutInMinutes: 20 + env: + # TODO(deepak1556): Remove this once runtime is updated for + # https://github.com/microsoft/vscode/issues/210467#issuecomment-2104566724 + UV_USE_IO_URING: 0 displayName: Run smoke tests (Remote) - script: | diff --git a/build/azure-pipelines/linux/setup-env.sh b/build/azure-pipelines/linux/setup-env.sh index e42a6b12b1f..9bfbf9ab41a 100755 --- a/build/azure-pipelines/linux/setup-env.sh +++ b/build/azure-pipelines/linux/setup-env.sh @@ -13,7 +13,7 @@ SYSROOT_ARCH="$SYSROOT_ARCH" node -e '(async () => { const { getVSCodeSysroot } if [ "$npm_config_arch" == "x64" ]; then if [ "$(echo "$@" | grep -c -- "--only-remote")" -eq 0 ]; then # Download clang based on chromium revision used by vscode - curl -s https://raw.githubusercontent.com/chromium/chromium/120.0.6099.268/tools/clang/scripts/update.py | python - --output-dir=$PWD/.build/CR_Clang --host-os=linux + curl -s https://raw.githubusercontent.com/chromium/chromium/122.0.6261.156/tools/clang/scripts/update.py | python - --output-dir=$PWD/.build/CR_Clang --host-os=linux # Download libcxx headers and objects from upstream electron releases DEBUG=libcxx-fetcher \ @@ -25,12 +25,12 @@ if [ "$npm_config_arch" == "x64" ]; then # Set compiler toolchain # Flags for the client build are based on - # https://source.chromium.org/chromium/chromium/src/+/refs/tags/120.0.6099.268:build/config/arm.gni - # https://source.chromium.org/chromium/chromium/src/+/refs/tags/120.0.6099.268:build/config/compiler/BUILD.gn - # https://source.chromium.org/chromium/chromium/src/+/refs/tags/120.0.6099.268:build/config/c++/BUILD.gn + # https://source.chromium.org/chromium/chromium/src/+/refs/tags/122.0.6261.156:build/config/arm.gni + # https://source.chromium.org/chromium/chromium/src/+/refs/tags/122.0.6261.156:build/config/compiler/BUILD.gn + # https://source.chromium.org/chromium/chromium/src/+/refs/tags/122.0.6261.156:build/config/c++/BUILD.gn export CC="$PWD/.build/CR_Clang/bin/clang --gcc-toolchain=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu" export CXX="$PWD/.build/CR_Clang/bin/clang++ --gcc-toolchain=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu" - export CXXFLAGS="-nostdinc++ -D__NO_INLINE__ -I$PWD/.build/libcxx_headers -isystem$PWD/.build/libcxx_headers/include -isystem$PWD/.build/libcxxabi_headers/include -fPIC -flto=thin -fsplit-lto-unit -D_LIBCPP_ABI_NAMESPACE=Cr --sysroot=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot" + export CXXFLAGS="-nostdinc++ -D__NO_INLINE__ -I$PWD/.build/libcxx_headers -isystem$PWD/.build/libcxx_headers/include -isystem$PWD/.build/libcxxabi_headers/include -fPIC -flto=thin -fsplit-lto-unit -D_LIBCPP_ABI_NAMESPACE=Cr -D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_EXTENSIVE --sysroot=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot" export LDFLAGS="-stdlib=libc++ --sysroot=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot -fuse-ld=lld -flto=thin -L$PWD/.build/libcxx-objects -lc++abi -L$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot/usr/lib/x86_64-linux-gnu -L$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot/lib/x86_64-linux-gnu -Wl,--lto-O0" # Set compiler toolchain for remote server diff --git a/build/azure-pipelines/product-compile.yml b/build/azure-pipelines/product-compile.yml index 5fd12caf017..41c33f3f265 100644 --- a/build/azure-pipelines/product-compile.yml +++ b/build/azure-pipelines/product-compile.yml @@ -104,11 +104,13 @@ steps: - script: yarn npm-run-all -lp core-ci-pr extensions-ci-pr hygiene eslint valid-layers-check vscode-dts-compile-check tsec-compile-check env: GITHUB_TOKEN: "$(github-distro-mixin-password)" + DISABLE_V8_COMPILE_CACHE: 1 # Disable v8 cache used by yarn v1.x, refs https://github.com/nodejs/node/issues/51555 displayName: Compile & Hygiene (OSS) - ${{ else }}: - script: yarn npm-run-all -lp core-ci extensions-ci hygiene eslint valid-layers-check vscode-dts-compile-check tsec-compile-check env: GITHUB_TOKEN: "$(github-distro-mixin-password)" + DISABLE_V8_COMPILE_CACHE: 1 # Disable v8 cache used by yarn v1.x, refs https://github.com/nodejs/node/issues/51555 displayName: Compile & Hygiene (non-OSS) - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: diff --git a/build/azure-pipelines/web/product-build-web.yml b/build/azure-pipelines/web/product-build-web.yml index 72ded6bcc11..bf43d9212cf 100644 --- a/build/azure-pipelines/web/product-build-web.yml +++ b/build/azure-pipelines/web/product-build-web.yml @@ -104,6 +104,7 @@ steps: tar --owner=0 --group=0 -czf $ARCHIVE_PATH -C .. vscode-web echo "##vso[task.setvariable variable=WEB_PATH]$ARCHIVE_PATH" env: + DISABLE_V8_COMPILE_CACHE: 1 # Disable v8 cache used by yarn v1.x, refs https://github.com/nodejs/node/issues/51555 GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Build diff --git a/build/azure-pipelines/win32/product-build-win32.yml b/build/azure-pipelines/win32/product-build-win32.yml index 3c92499b2a6..d3827b930f8 100644 --- a/build/azure-pipelines/win32/product-build-win32.yml +++ b/build/azure-pipelines/win32/product-build-win32.yml @@ -93,16 +93,10 @@ steps: . build/azure-pipelines/win32/exec.ps1 . build/azure-pipelines/win32/retry.ps1 $ErrorActionPreference = "Stop" - # TODO: remove custom node-gyp when updating to Node v20, - # refs https://github.com/npm/cli/releases/tag/v10.2.3 which is available with Node >= 20.10.0 - $nodeGypDir = "$(Agent.TempDirectory)/custom-packages" - mkdir "$nodeGypDir" - npm install node-gyp@10.0.1 -g --prefix "$nodeGypDir" - $env:npm_config_node_gyp = "${nodeGypDir}/node_modules/node-gyp/bin/node-gyp.js" - $env:npm_config_arch = "$(VSCODE_ARCH)" - $env:CHILD_CONCURRENCY="1" retry { exec { yarn --frozen-lockfile --check-files } } env: + npm_config_arch: $(VSCODE_ARCH) + CHILD_CONCURRENCY: 1 ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 GITHUB_TOKEN: "$(github-distro-mixin-password)" diff --git a/build/checksums/electron.txt b/build/checksums/electron.txt index 86f78d0adea..88fc9eceff0 100644 --- a/build/checksums/electron.txt +++ b/build/checksums/electron.txt @@ -1,75 +1,75 @@ -69b40637a88ad4c17877b3d665b39ad0e11928aa71b19ef45f5b76250d1c9786 *chromedriver-v28.2.8-darwin-arm64.zip -3a9ce6179228245f2c7878c4238e10d51c77dc20642922a226ccc235a20f5a29 *chromedriver-v28.2.8-darwin-x64.zip -7f6470ea5d86dbe68fcc3fccfefd3b7135ba3468ef54b0235bf57cedeabf433d *chromedriver-v28.2.8-linux-arm64.zip -4bfe709d58b237f5c5a7618b2abecf533dac9415d327e763ad6cf622218517cc *chromedriver-v28.2.8-linux-armv7l.zip -7558ee413f96f88b9b9ad5787dd433adcfaf56411fdf052826d39d204ebaba9d *chromedriver-v28.2.8-linux-x64.zip -9814583b075d969c32afb6e929b4bf7956b0223fded996c91341388b8f638dd6 *chromedriver-v28.2.8-mas-arm64.zip -82d11c6606db9aea355b1e410083c72bd1e39abb9e34a839c16b16b75364ea0d *chromedriver-v28.2.8-mas-x64.zip -4803a5335a40ba208136094f5adfde2c4272761d34e0e9e9f4febc2ef676c3ad *chromedriver-v28.2.8-win32-arm64.zip -7b079f47869f7e96a5829f6fb7eff032394f76218b39a2aaf73cc93ce8a68050 *chromedriver-v28.2.8-win32-ia32.zip -2aedd176d4f72b29cd1914364e813756d52f53558df32e3429996b820edc994d *chromedriver-v28.2.8-win32-x64.zip -ae1a521aa36053a3b60b318d7bc093ec7579af6aa8b02bffe1f9e70d6922b726 *electron-api.json -a916f0cc438258f42f43955157565e7eca14966266f3fb123c8c736bece97daa *electron-v28.2.8-darwin-arm64-dsym-snapshot.zip -3c31d0a105b0632f15aa8adc68f06dc8ca47b1fdf1e62d1436ac43af117a22fb *electron-v28.2.8-darwin-arm64-dsym.zip -dab03f1cd7b499552d503bcca2fc1c3f40a1d2c463655ca3ace20778f08e9b04 *electron-v28.2.8-darwin-arm64-symbols.zip -2965d8c8d64fb6c51f5a283a246de653bfae22fe4bf9adf6c04592afabf62f04 *electron-v28.2.8-darwin-arm64.zip -03511a34d94d27eb576ab20e3a432c082a32a298475c7a85a329e029dddc55e4 *electron-v28.2.8-darwin-x64-dsym-snapshot.zip -96089786bd2723786673561c9b6f9a154928de663f2411f10153e6c985703eef *electron-v28.2.8-darwin-x64-dsym.zip -872789c3c218ab8f98be83c7781e3e6ef0114bd39780d65eaae77e99dbbda1de *electron-v28.2.8-darwin-x64-symbols.zip -a7889addd37254f842798bdd3ca34752b75acf6d8dd456cdeb2d75590c0a9ceb *electron-v28.2.8-darwin-x64.zip -fb90b8c903407ae575f9c8f727376519c0b35ed6f01dec55b177285b5db864e3 *electron-v28.2.8-linux-arm64-debug.zip -591248f7c94a6d7c4a4d8b2fcf63c8e4347018a65e1f68ed90e5549a587062c8 *electron-v28.2.8-linux-arm64-symbols.zip -6183db1029cebd9e0fb0e4f2d24a80b0274c5265756e66cb9fa0a480b92c98ea *electron-v28.2.8-linux-arm64.zip -fb90b8c903407ae575f9c8f727376519c0b35ed6f01dec55b177285b5db864e3 *electron-v28.2.8-linux-armv7l-debug.zip -87c4c534cd1d447b9d4632585a0d79c9d31114bd39ca63df1f2384afae3aa6b7 *electron-v28.2.8-linux-armv7l-symbols.zip -2a772b65815a0d47a756eed52f76cd9f27a8c277d7998bfcfe93b84a346eb255 *electron-v28.2.8-linux-armv7l.zip -773aa1f0bbe2b79765bf498958565f63957f8ec2e42327978a143dcbbc7f1bea *electron-v28.2.8-linux-x64-debug.zip -f8cbc6f2b719cc2f623afcfde8cb1d42614708793621a7a97b328015366b9b8f *electron-v28.2.8-linux-x64-symbols.zip -e7d17ee311299dfef3d2916987a513c4c1b66ad2e417c15fa5d29699602bd6cb *electron-v28.2.8-linux-x64.zip -5f0179fd7bf3927381bde24c9fb372fe95328be0500918cd6ee7f9503fae1ef5 *electron-v28.2.8-mas-arm64-dsym-snapshot.zip -e9810019f1d7b1b5a93fd1aee8adda5a872ebfb170de6d55cdd55162b923432d *electron-v28.2.8-mas-arm64-dsym.zip -4781376244c7df89d119575e2788ad43fae4387d850ef672665688081b30997c *electron-v28.2.8-mas-arm64-symbols.zip -a3932199781970e0b2fdb805d6556287ca877b35ac19384da00474140e14c41f *electron-v28.2.8-mas-arm64.zip -326cde32079496e0d976c5b65e85e5ce208eea3d8d23cd92c9e25f0fa6b30f40 *electron-v28.2.8-mas-x64-dsym-snapshot.zip -59a2b3d28dba45ee3016f8ab49a71b0c55f99ef046476183bc36890c9d335a71 *electron-v28.2.8-mas-x64-dsym.zip -313ff88f568c39079a1b7a1011f77fa03890cb9bb53649a489643311303cc3b8 *electron-v28.2.8-mas-x64-symbols.zip -41ab9f3addea5066d7e0ace28ebaead7128a2073931473c847aa9133b7df9248 *electron-v28.2.8-mas-x64.zip -179de6dd4835216bcd3e8bb9eb4d4b54013df865f52dbf0d5214726fc31cba9a *electron-v28.2.8-win32-arm64-pdb.zip -8628dec571206001420c1d8655904883d5de7e772d51ab2101b002c22e0dd25c *electron-v28.2.8-win32-arm64-symbols.zip -c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v28.2.8-win32-arm64-toolchain-profile.zip -bb2a2a466d14c32c06ff09c42b3d1413f19fdc8a49a445d07d289fa453c268d3 *electron-v28.2.8-win32-arm64.zip -1d1efc3a1d17072bc76a4a63c8236a896d46f6f3badacd50bc5824149196d56f *electron-v28.2.8-win32-ia32-pdb.zip -9ddb1520de421a7c636160d01432c9bf111e6ef4b9a3be41b185c702c72353ac *electron-v28.2.8-win32-ia32-symbols.zip -c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v28.2.8-win32-ia32-toolchain-profile.zip -38e22f9b0a32e0fc26e81905214e244c0a5d5c19e13c8ca2329ac75b62881472 *electron-v28.2.8-win32-ia32.zip -8168296e0454377e0113a7d0f87535d3d0e0c1a8538e8079ee1aae9c7223bb02 *electron-v28.2.8-win32-x64-pdb.zip -a276e9e748fa7db970e7dcce6f4ae571d8615a44e5208c0fa3c03de08774a4aa *electron-v28.2.8-win32-x64-symbols.zip -c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v28.2.8-win32-x64-toolchain-profile.zip -079cc98f7933992ac7154e21e160d4a4c6b3541c26b56fc6f8438e9eabc369b9 *electron-v28.2.8-win32-x64.zip -f838e4a7c24518c5fa25d4a23acf869737cfa88761019cea4f83ebfb302363ec *electron.d.ts -4450bcc66cece4ff2373563e0123799f95645fa155577a8f380211b29e8b4ec9 *ffmpeg-v28.2.8-darwin-arm64.zip -152e3ed53098d24f356d7ec640d19efc57f7f34c39d8b8278f2586985d4a99a1 *ffmpeg-v28.2.8-darwin-x64.zip -8e108e533811febcc51f377ac8604d506663453e41c02dc818517e1ea9a4e8d5 *ffmpeg-v28.2.8-linux-arm64.zip -51ecd03435f56a2ced31b1c9dbf281955ba82a814ca0214a4292bdc711e5a45c *ffmpeg-v28.2.8-linux-armv7l.zip -acc9dc3765f68b7563045e2d0df11bbef6b41be0a1c34bbf9fa778f36eefb42f *ffmpeg-v28.2.8-linux-x64.zip -15a2a4a28a66e65122eb4f2bd796ccd5b6ed45420a034878affd002fc8c290dc *ffmpeg-v28.2.8-mas-arm64.zip -2dfe2f524c5220f50c7b6fe08605a67631b5520e0c82842e1f41f677cac17643 *ffmpeg-v28.2.8-mas-x64.zip -313e2979f0df88715159c0737bfbb5ae1d5c79fb9820e94d2a93ba71d3324ecd *ffmpeg-v28.2.8-win32-arm64.zip -9e73bc07563aefa8b9625676939a410b35a823d961b96da0e8edd90d7e5fb47b *ffmpeg-v28.2.8-win32-ia32.zip -1b11042defc8a3f403e5567fa4a4b8c59b224f3b7b52d44d6c7197b96af7b53b *ffmpeg-v28.2.8-win32-x64.zip -1e2e9480d4228f6bbc731ff7ee413b9e97656c36b15418d20681a76d82902b86 *hunspell_dictionaries.zip -8c8b967cf4c78ed9bbf4921b2c616257f45b137412eb3bc64176066c3e47bbe8 *libcxx-objects-v28.2.8-linux-arm64.zip -56af259535ccfaac295b82ce68686f9582265cb2ebe2783852f518c0fabc8a1e *libcxx-objects-v28.2.8-linux-armv7l.zip -b590e001dc98e32e5952ca69573e6f1bcec5e2f2d99052d1089ab72084cccea1 *libcxx-objects-v28.2.8-linux-x64.zip -c0634d5c92f0a2983b17c866f7d3694cb75f6e78cd07b10d9488ef46acc66a50 *libcxx_headers.zip -99ee16441d9eb2b92a05d5a5c9b9dc4cdfab33cb09595e9d78fd2ba503dead5b *libcxxabi_headers.zip -a95de1da301d641caaafaea9869c4c7834c254f818ac0c10d97402b2220c8be3 *mksnapshot-v28.2.8-darwin-arm64.zip -e5ef6b35d7cd807f93babfedbbde513ab6053ad9fb80b0f7abc1bfda414daaa1 *mksnapshot-v28.2.8-darwin-x64.zip -eeb6c5b7962af8d5cfaa97b2cf96d312d0ad57a3abb3e00774d50ea2e005bb9b *mksnapshot-v28.2.8-linux-arm64-x64.zip -0adacd0767469f90400b1f17ba8ac3ccb33cfeb11a8ef54d70bc8adb7cc306dc *mksnapshot-v28.2.8-linux-armv7l-x64.zip -5242817f1f26e10804e7e2446d0a8a64e8b2958cdba01e79d89db883d9d960d0 *mksnapshot-v28.2.8-linux-x64.zip -0ecb67673508c10f4fe08e7cb80300b9a8f507f50994c79caf302ff78ef748ca *mksnapshot-v28.2.8-mas-arm64.zip -19429da56077f12de4d4563f49c55f4f1f0fe61f66863804640fc55e65ee98f9 *mksnapshot-v28.2.8-mas-x64.zip -c7b47ae63c2f6eb07b06379206e6f215fbcb2b9a49faa72ca850bf8f9b998c4c *mksnapshot-v28.2.8-win32-arm64-x64.zip -0032660a9f8575a153951f29adae49a18e400b40906eec803fe7e3d2e970503d *mksnapshot-v28.2.8-win32-ia32.zip -2c71c9a2bd4441e580dc3083073e712fba94e0236415c8ab35320da52f492508 *mksnapshot-v28.2.8-win32-x64.zip +e59378f63e935a6a561e272cdf44a8c5c3f4c56a8ff5ed0b33d45f18dd7d0d6c *chromedriver-v29.3.1-darwin-arm64.zip +4c4b2f11e9a396ff0e4c2282f4afe898f548af5e530a26c4c52fc7dbe307eb31 *chromedriver-v29.3.1-darwin-x64.zip +10b0d4a01636ae1f064cb950d5cff2a591dff2d2573fa9169335a492815169d3 *chromedriver-v29.3.1-linux-arm64.zip +45aff39d150dd423536d221bcdf2dab12cef4d0e8df50dddcda0387f60c70843 *chromedriver-v29.3.1-linux-armv7l.zip +569022d7a6fc4634ee4f496bb0414b7a8b34e505e22c2d423e915776e23d576a *chromedriver-v29.3.1-linux-x64.zip +d2090eb226eb0fef894837277d08a313af62da5807ab14d4aae7e6ba0a6a8466 *chromedriver-v29.3.1-mas-arm64.zip +3d425b6713d2a6e3149c4559cff76940e0443e236e61c7ba9b35ce2438f7de15 *chromedriver-v29.3.1-mas-x64.zip +5a95303fffbab24b07e842441e677ba98966dd800c90a7e842a97e43f7681cd6 *chromedriver-v29.3.1-win32-arm64.zip +553f8a81b0974c23eb473d5129450413f206e67128f89b7f7723ae76f9e8ec5d *chromedriver-v29.3.1-win32-ia32.zip +67f2f561703c6008c1c51dfd50be991752dfa3959bf5bb5a3a324143894fdcc5 *chromedriver-v29.3.1-win32-x64.zip +1e8366964ae298ec1e5e67b3f192c1d7a7cffc1b932b2f32fac3d075962c8f7e *electron-api.json +80596ef89f4638495bb24a92b75191bb0b61151e3cbc608090c1e406d14cafd5 *electron-v29.3.1-darwin-arm64-dsym-snapshot.zip +a2804d07dded66a5735aa1d1e5c547ea97bff09e2f1443c019ba564a33a5660b *electron-v29.3.1-darwin-arm64-dsym.zip +4dd9f6c00f2021dba34532452eecc15ce7e5eb914978319fd03246d19ff66baa *electron-v29.3.1-darwin-arm64-symbols.zip +aaada7a9f7ee72cd2a9a465ed0b8ec703aeedda9084f67cf72c1dce8e2aff7ca *electron-v29.3.1-darwin-arm64.zip +cab2c8a7a72c6e6b59e04e3292f27799b4a25592764960d9df4894fab405abc5 *electron-v29.3.1-darwin-x64-dsym-snapshot.zip +f7706f674d092f314fb30e85985d3172c1a125804e8132b206b65196b8dc81c5 *electron-v29.3.1-darwin-x64-dsym.zip +f00ec2929503e067b4ee59f8c38d1d2419db5c2af3c2b078d30d17faae8dbd5d *electron-v29.3.1-darwin-x64-symbols.zip +be6b70648d35959d346924e89aff5419af321c80f929d0e252fba131d9c93f50 *electron-v29.3.1-darwin-x64.zip +19f8b15ff1eb3a572adab73444c8b12f9815fa8ddaadbd8383ef5bb7370f98cc *electron-v29.3.1-linux-arm64-debug.zip +db0861e5d285428cc98de1f055fd7ef2fb2b331ebfd3e0a069bdf136b5bdc5c7 *electron-v29.3.1-linux-arm64-symbols.zip +d900a5597e296cb925dc2e6266b1d839b0254ab12e424d405785d6e351f1c4d7 *electron-v29.3.1-linux-arm64.zip +19f8b15ff1eb3a572adab73444c8b12f9815fa8ddaadbd8383ef5bb7370f98cc *electron-v29.3.1-linux-armv7l-debug.zip +8936bb96a59c1ac129555050ae00b478bbc6c16a0e759ed07231624b3ac52749 *electron-v29.3.1-linux-armv7l-symbols.zip +2a66d5603cf59a28699e4465488032f1dfac6118140ed129cf7403617329f983 *electron-v29.3.1-linux-armv7l.zip +a1f7984c302b2f7a03e836a7a6026d8ba64ca7806f47cd7b9dcc2e744680fd7c *electron-v29.3.1-linux-x64-debug.zip +fe2f5a78e7c485423fae7d204f6ba7bea95f9427703e97831a5555ab42ca93f3 *electron-v29.3.1-linux-x64-symbols.zip +d907e1c8074d2b7933d8b7525da3987f88d5b5ecf88131efec3eb5bd710a15b4 *electron-v29.3.1-linux-x64.zip +7cd32474a7c024d40ae9f17fe83678ae34b6f631889e895cabed87d0ee1781bf *electron-v29.3.1-mas-arm64-dsym-snapshot.zip +0058c71c614e252b4ad689de8a492563ebf938ba90cdb124e5454a9ec9d4c75c *electron-v29.3.1-mas-arm64-dsym.zip +aafedfab99d059079011139cf534bbfca44d50cd0a668b0ca547aeb8eed99c14 *electron-v29.3.1-mas-arm64-symbols.zip +f45417c845be012f0a9d3b8c92d5d3d5b4b9650e06809a9d783baa1ff8ce75a7 *electron-v29.3.1-mas-arm64.zip +8a91e7cced48162ec60368a992e8c53ef7e0ba574ce0c6edd8462167ba23b053 *electron-v29.3.1-mas-x64-dsym-snapshot.zip +4bfdd08bdb98afd3966e6a9c506dbb6e8bbbed7b0b22c7ae019b3cf8564e8354 *electron-v29.3.1-mas-x64-dsym.zip +dada1302a225509de9e031c8b139096a28398f883c8bdcae4b8fc3a92dc4c99d *electron-v29.3.1-mas-x64-symbols.zip +555d83c9eea2c1dc40c6996092ef2eb812d9d4937062d53d8909bf2a9432ca88 *electron-v29.3.1-mas-x64.zip +857bcb8f8866b2183355f71e968a690ec7d9ecc386b507988fe0ab560fe25a57 *electron-v29.3.1-win32-arm64-pdb.zip +a38ee738e44c3a9470ff765422410c59bd3c0a94a955e3b1ce661b68d094de18 *electron-v29.3.1-win32-arm64-symbols.zip +c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v29.3.1-win32-arm64-toolchain-profile.zip +74bcf7b7bb09c6311a5ef01eef41e20eccb84cd169651138234a332ad33aa087 *electron-v29.3.1-win32-arm64.zip +4650398c9c49b63050b4c2d28ae664c1d14912464a2744170338c131291aa290 *electron-v29.3.1-win32-ia32-pdb.zip +79a7a2db4c26c231d0963a924b129391cf920cd6b97d28ef095a2a1da4e14577 *electron-v29.3.1-win32-ia32-symbols.zip +c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v29.3.1-win32-ia32-toolchain-profile.zip +5527aa7d73b49d1c3298d9f2fc930be775e7d093a70bb613bec73e2ddd316afa *electron-v29.3.1-win32-ia32.zip +d85bc6393bd5890cf0bc616c41c2a5c0596ea4c3967d51bbf146a12cae727fad *electron-v29.3.1-win32-x64-pdb.zip +870bc19b8f38a84eb65fa6269fe2026b8dd8b76f9bbeded15c7313923f3e2c66 *electron-v29.3.1-win32-x64-symbols.zip +c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v29.3.1-win32-x64-toolchain-profile.zip +ccd465a085578168b6bf88ac76a5946f649e977efa7ef130c460b04df1becffa *electron-v29.3.1-win32-x64.zip +eab0311367f1e6b264ac788ac7291449d50bddf0049015391370a2eac462c320 *electron.d.ts +1718c59d8a963ef09325d300e10684d1a2419c186f0c70ac200d03b4142cdbfd *ffmpeg-v29.3.1-darwin-arm64.zip +a58339efba05ff93ca39e3000ec5aa5c81fb059c8786401324285defad11eb4b *ffmpeg-v29.3.1-darwin-x64.zip +4e2ba537d7c131abbd34168bce2c28cc9ef6262b217d5f4085afccfdf9635da6 *ffmpeg-v29.3.1-linux-arm64.zip +4aa56ad5d849f4e61af22678a179346b68aec9100282e1b8a43df25d95721677 *ffmpeg-v29.3.1-linux-armv7l.zip +0558e6e1f78229d303e16d4d8c290794baa9adc619fdd2ddccadb3ea241a1df4 *ffmpeg-v29.3.1-linux-x64.zip +a580fce86cd20aaee06ad4136b2dfbbf7a7449be8fc4d1a528535b2a83b067d4 *ffmpeg-v29.3.1-mas-arm64.zip +41d8a8d20429ea22bbfdd482f1ea2c265cabdca3cacb35737be01f427455204e *ffmpeg-v29.3.1-mas-x64.zip +ce3bf67555cf614c837d1bc80aaa071627750e3ffcde03a25b750796de23fd43 *ffmpeg-v29.3.1-win32-arm64.zip +f305313a1c3d15c6308c6158edc2d9cadc7465536adefa57fa31d879e6fe5e55 *ffmpeg-v29.3.1-win32-ia32.zip +8c6b7febbd80e53ea0cf0e89104006b4211b96d0d05933f517d69d0d578e8726 *ffmpeg-v29.3.1-win32-x64.zip +59827658661e330bc4ef876419c927a647a9c393aac2e7767887ae0ac600dd65 *hunspell_dictionaries.zip +1a5ec4216f0f938be6ae45853ffa032cfeb04409f757e9b66228362fe14da74f *libcxx-objects-v29.3.1-linux-arm64.zip +febbdade1c2958dc24498d62e6402bc1a9add49c3837487cc8ecb0ecb0f28459 *libcxx-objects-v29.3.1-linux-armv7l.zip +d6c5c7e67f8e50cad64493215b418a303cb5a30e39800c85d28401f66e1addb3 *libcxx-objects-v29.3.1-linux-x64.zip +57f87572e20185f329334ca9c6971bb7974424fb5ff4aa2e11f3a8668f8060f9 *libcxx_headers.zip +c7bcb0555dd10aed27ec7041338783df430e58da79ccba6863cbfb8cd89ef062 *libcxxabi_headers.zip +6962a9872def625e43d87ea4f50e90bc571d5b522abe7b9b33ce17714c43a05e *mksnapshot-v29.3.1-darwin-arm64.zip +b801394b60eb4cefe52fddd35616be9e79058b53d2860badb1daa0c61242b730 *mksnapshot-v29.3.1-darwin-x64.zip +906058eaeadb81f918962529185b2cf4b5fc6b13adbbf04fea7fd4ace9c1f20e *mksnapshot-v29.3.1-linux-arm64-x64.zip +74c9834b9d8237b001cbe5822b2426bebe910a72c1173377c1cd446f726bc2e7 *mksnapshot-v29.3.1-linux-armv7l-x64.zip +6eafdfcbdc44d48df267ddb09e09e0acda1abbe472211e947ce4b68852f99d52 *mksnapshot-v29.3.1-linux-x64.zip +04164d534fae6f12ad37e2f2268ec864b3c4417c08134edeeb555dd1bf73c073 *mksnapshot-v29.3.1-mas-arm64.zip +3a870add5f6c3287f9f958d0327f67d59ef0a9c30bfb94fb7155b6ee6e905e46 *mksnapshot-v29.3.1-mas-x64.zip +174ac9f2d8b4664587a983067c2b870b6e74fe8079715a502c11d55d774d6317 *mksnapshot-v29.3.1-win32-arm64-x64.zip +50efe4e272a54d04392ccd8a205164f4c1400c8197648d1a1fa71e667e37d7ff *mksnapshot-v29.3.1-win32-ia32.zip +e80d3e57ed67f05573a12a65b0be30cdb3e0b147cf817a80eb89b9886e7743ae *mksnapshot-v29.3.1-win32-x64.zip diff --git a/build/checksums/nodejs.txt b/build/checksums/nodejs.txt index 13aa4c7e87b..63d24a93ab6 100644 --- a/build/checksums/nodejs.txt +++ b/build/checksums/nodejs.txt @@ -1,6 +1,7 @@ -9f982cc91b28778dd8638e4f94563b0c2a1da7aba62beb72bd427721035ab553 node-v18.18.2-darwin-arm64.tar.gz -5bb8da908ed590e256a69bf2862238c8a67bc4600119f2f7721ca18a7c810c0f node-v18.18.2-darwin-x64.tar.gz -0c9a6502b66310cb26e12615b57304e91d92ac03d4adcb91c1906351d7928f0d node-v18.18.2-linux-arm64.tar.gz -7a3b34a6fdb9514bc2374114ec6df3c36113dc5075c38b22763aa8f106783737 node-v18.18.2-linux-armv7l.tar.gz -a44c3e7f8bf91e852c928e5d8bd67ca316b35e27eec1d8acbe3b9dbe03688dab node-v18.18.2-linux-x64.tar.gz -54884183ff5108874c091746465e8156ae0acc68af589cc10bc41b3927db0f4a win-x64/node.exe +31d2d46ae8d8a3982f54e2ff1e60c2e4a8e80bf78a3e8b46dcaac95ac5d7ce6a node-v20.9.0-darwin-arm64.tar.gz +fc5b73f2a78c17bbe926cdb1447d652f9f094c79582f1be6471b4b38a2e1ccc8 node-v20.9.0-darwin-x64.tar.gz +d2a7dbeeb274bfd16b579d2cafb92f673010df36c83a5b55de3916aad6806a6a node-v20.9.0-linux-arm64.tar.gz +a28a0de05177106d241ef426b3e006022bc7d242224adace7565868bd9ee6c06 node-v20.9.0-linux-armv7l.tar.gz +f0919f092fbf74544438907fa083c21e76b2d7a4bc287f0607ada1553ef16f60 node-v20.9.0-linux-x64.tar.gz +54e165b89e75158993910053db5b0e652c1826521e624126de5ca6de9ff7b06d win-arm64/node.exe +538140015da83597ea7e7ef5e108ebac8a2dc4784b2a4134222b6c27c39f90ad win-x64/node.exe diff --git a/build/lib/layersChecker.js b/build/lib/layersChecker.js index 4979682935e..dce2b85d658 100644 --- a/build/lib/layersChecker.js +++ b/build/lib/layersChecker.js @@ -62,7 +62,13 @@ const CORE_TYPES = [ 'EventTarget', 'BroadcastChannel', 'performance', - 'Blob' + 'Blob', + 'crypto', + 'File', + 'fetch', + 'RequestInit', + 'Headers', + 'Response' ]; // Types that are defined in a common layer but are known to be only // available in native environments should not be allowed in browser @@ -164,6 +170,62 @@ const RULES = [ '@types/node' // no node.js ] }, + // Common: vs/workbench/api/common/extHostTypes.ts + { + target: '**/vs/workbench/api/common/extHostTypes.ts', + allowedTypes: [ + ...CORE_TYPES, + // Safe access to global + '__global' + ], + disallowedTypes: NATIVE_TYPES, + disallowedDefinitions: [ + 'lib.dom.d.ts', // no DOM + '@types/node' // no node.js + ] + }, + // Common: vs/workbench/api/common/extHostChatAgents2.ts + { + target: '**/vs/workbench/api/common/extHostChatAgents2.ts', + allowedTypes: [ + ...CORE_TYPES, + // Safe access to global + '__global' + ], + disallowedTypes: NATIVE_TYPES, + disallowedDefinitions: [ + 'lib.dom.d.ts', // no DOM + '@types/node' // no node.js + ] + }, + // Common: vs/workbench/api/common/extHostChatVariables.ts + { + target: '**/vs/workbench/api/common/extHostChatVariables.ts', + allowedTypes: [ + ...CORE_TYPES, + // Safe access to global + '__global' + ], + disallowedTypes: NATIVE_TYPES, + disallowedDefinitions: [ + 'lib.dom.d.ts', // no DOM + '@types/node' // no node.js + ] + }, + // Common: vs/workbench/api/common/extensionHostMain.ts + { + target: '**/vs/workbench/api/common/extensionHostMain.ts', + allowedTypes: [ + ...CORE_TYPES, + // Safe access to global + '__global' + ], + disallowedTypes: NATIVE_TYPES, + disallowedDefinitions: [ + 'lib.dom.d.ts', // no DOM + '@types/node' // no node.js + ] + }, // Common { target: '**/vs/**/common/**', diff --git a/build/lib/layersChecker.ts b/build/lib/layersChecker.ts index 864d61f1452..039f222135d 100644 --- a/build/lib/layersChecker.ts +++ b/build/lib/layersChecker.ts @@ -63,7 +63,13 @@ const CORE_TYPES = [ 'EventTarget', 'BroadcastChannel', 'performance', - 'Blob' + 'Blob', + 'crypto', + 'File', + 'fetch', + 'RequestInit', + 'Headers', + 'Response' ]; // Types that are defined in a common layer but are known to be only @@ -179,6 +185,70 @@ const RULES: IRule[] = [ ] }, + // Common: vs/workbench/api/common/extHostTypes.ts + { + target: '**/vs/workbench/api/common/extHostTypes.ts', + allowedTypes: [ + ...CORE_TYPES, + + // Safe access to global + '__global' + ], + disallowedTypes: NATIVE_TYPES, + disallowedDefinitions: [ + 'lib.dom.d.ts', // no DOM + '@types/node' // no node.js + ] + }, + + // Common: vs/workbench/api/common/extHostChatAgents2.ts + { + target: '**/vs/workbench/api/common/extHostChatAgents2.ts', + allowedTypes: [ + ...CORE_TYPES, + + // Safe access to global + '__global' + ], + disallowedTypes: NATIVE_TYPES, + disallowedDefinitions: [ + 'lib.dom.d.ts', // no DOM + '@types/node' // no node.js + ] + }, + + // Common: vs/workbench/api/common/extHostChatVariables.ts + { + target: '**/vs/workbench/api/common/extHostChatVariables.ts', + allowedTypes: [ + ...CORE_TYPES, + + // Safe access to global + '__global' + ], + disallowedTypes: NATIVE_TYPES, + disallowedDefinitions: [ + 'lib.dom.d.ts', // no DOM + '@types/node' // no node.js + ] + }, + + // Common: vs/workbench/api/common/extensionHostMain.ts + { + target: '**/vs/workbench/api/common/extensionHostMain.ts', + allowedTypes: [ + ...CORE_TYPES, + + // Safe access to global + '__global' + ], + disallowedTypes: NATIVE_TYPES, + disallowedDefinitions: [ + 'lib.dom.d.ts', // no DOM + '@types/node' // no node.js + ] + }, + // Common { target: '**/vs/**/common/**', diff --git a/build/linux/dependencies-generator.js b/build/linux/dependencies-generator.js index 80c247d1129..bff0c9a25df 100644 --- a/build/linux/dependencies-generator.js +++ b/build/linux/dependencies-generator.js @@ -23,7 +23,7 @@ const product = require("../../product.json"); // The reference dependencies, which one has to update when the new dependencies // are valid, are in dep-lists.ts const FAIL_BUILD_FOR_NEW_DEPENDENCIES = true; -// Based on https://source.chromium.org/chromium/chromium/src/+/refs/tags/120.0.6099.268:chrome/installer/linux/BUILD.gn;l=64-80 +// Based on https://source.chromium.org/chromium/chromium/src/+/refs/tags/122.0.6261.156:chrome/installer/linux/BUILD.gn;l=64-80 // and the Linux Archive build // Shared library dependencies that we already bundle. const bundledDeps = [ diff --git a/build/linux/dependencies-generator.ts b/build/linux/dependencies-generator.ts index 9f1a068b8d7..226310e1258 100644 --- a/build/linux/dependencies-generator.ts +++ b/build/linux/dependencies-generator.ts @@ -25,7 +25,7 @@ import product = require('../../product.json'); // are valid, are in dep-lists.ts const FAIL_BUILD_FOR_NEW_DEPENDENCIES: boolean = true; -// Based on https://source.chromium.org/chromium/chromium/src/+/refs/tags/120.0.6099.268:chrome/installer/linux/BUILD.gn;l=64-80 +// Based on https://source.chromium.org/chromium/chromium/src/+/refs/tags/122.0.6261.156:chrome/installer/linux/BUILD.gn;l=64-80 // and the Linux Archive build // Shared library dependencies that we already bundle. const bundledDeps = [ diff --git a/build/linux/rpm/dep-lists.js b/build/linux/rpm/dep-lists.js index bd84fc146dc..5bdcac609c8 100644 --- a/build/linux/rpm/dep-lists.js +++ b/build/linux/rpm/dep-lists.js @@ -38,10 +38,12 @@ exports.referenceGeneratedDepsByArch = { 'libc.so.6()(64bit)', 'libc.so.6(GLIBC_2.10)(64bit)', 'libc.so.6(GLIBC_2.11)(64bit)', + 'libc.so.6(GLIBC_2.12)(64bit)', 'libc.so.6(GLIBC_2.14)(64bit)', 'libc.so.6(GLIBC_2.15)(64bit)', 'libc.so.6(GLIBC_2.16)(64bit)', 'libc.so.6(GLIBC_2.17)(64bit)', + 'libc.so.6(GLIBC_2.18)(64bit)', 'libc.so.6(GLIBC_2.2.5)(64bit)', 'libc.so.6(GLIBC_2.28)(64bit)', 'libc.so.6(GLIBC_2.3)(64bit)', @@ -134,9 +136,12 @@ exports.referenceGeneratedDepsByArch = { 'libc.so.6', 'libc.so.6(GLIBC_2.10)', 'libc.so.6(GLIBC_2.11)', + 'libc.so.6(GLIBC_2.12)', + 'libc.so.6(GLIBC_2.14)', 'libc.so.6(GLIBC_2.15)', 'libc.so.6(GLIBC_2.16)', 'libc.so.6(GLIBC_2.17)', + 'libc.so.6(GLIBC_2.18)', 'libc.so.6(GLIBC_2.28)', 'libc.so.6(GLIBC_2.4)', 'libc.so.6(GLIBC_2.6)', @@ -237,6 +242,7 @@ exports.referenceGeneratedDepsByArch = { 'libatspi.so.0()(64bit)', 'libc.so.6()(64bit)', 'libc.so.6(GLIBC_2.17)(64bit)', + 'libc.so.6(GLIBC_2.18)(64bit)', 'libc.so.6(GLIBC_2.28)(64bit)', 'libcairo.so.2()(64bit)', 'libcurl.so.4()(64bit)', diff --git a/build/linux/rpm/dep-lists.ts b/build/linux/rpm/dep-lists.ts index 82a4fe7698d..3eb2239aa00 100644 --- a/build/linux/rpm/dep-lists.ts +++ b/build/linux/rpm/dep-lists.ts @@ -37,10 +37,12 @@ export const referenceGeneratedDepsByArch = { 'libc.so.6()(64bit)', 'libc.so.6(GLIBC_2.10)(64bit)', 'libc.so.6(GLIBC_2.11)(64bit)', + 'libc.so.6(GLIBC_2.12)(64bit)', 'libc.so.6(GLIBC_2.14)(64bit)', 'libc.so.6(GLIBC_2.15)(64bit)', 'libc.so.6(GLIBC_2.16)(64bit)', 'libc.so.6(GLIBC_2.17)(64bit)', + 'libc.so.6(GLIBC_2.18)(64bit)', 'libc.so.6(GLIBC_2.2.5)(64bit)', 'libc.so.6(GLIBC_2.28)(64bit)', 'libc.so.6(GLIBC_2.3)(64bit)', @@ -133,9 +135,12 @@ export const referenceGeneratedDepsByArch = { 'libc.so.6', 'libc.so.6(GLIBC_2.10)', 'libc.so.6(GLIBC_2.11)', + 'libc.so.6(GLIBC_2.12)', + 'libc.so.6(GLIBC_2.14)', 'libc.so.6(GLIBC_2.15)', 'libc.so.6(GLIBC_2.16)', 'libc.so.6(GLIBC_2.17)', + 'libc.so.6(GLIBC_2.18)', 'libc.so.6(GLIBC_2.28)', 'libc.so.6(GLIBC_2.4)', 'libc.so.6(GLIBC_2.6)', @@ -236,6 +241,7 @@ export const referenceGeneratedDepsByArch = { 'libatspi.so.0()(64bit)', 'libc.so.6()(64bit)', 'libc.so.6(GLIBC_2.17)(64bit)', + 'libc.so.6(GLIBC_2.18)(64bit)', 'libc.so.6(GLIBC_2.28)(64bit)', 'libcairo.so.2()(64bit)', 'libcurl.so.4()(64bit)', diff --git a/build/npm/preinstall.js b/build/npm/preinstall.js index edf0d98c3d5..cfb5bb2985f 100644 --- a/build/npm/preinstall.js +++ b/build/npm/preinstall.js @@ -10,13 +10,10 @@ const minorNodeVersion = parseInt(nodeVersion[2]); const patchNodeVersion = parseInt(nodeVersion[3]); if (!process.env['VSCODE_SKIP_NODE_VERSION_CHECK']) { - if (majorNodeVersion < 18 || (majorNodeVersion === 18 && minorNodeVersion < 15)) { - console.error('\x1b[1;31m*** Please use node.js versions >=18.15.x and <19.\x1b[0;0m'); + if (majorNodeVersion < 20) { + console.error('\x1b[1;31m*** Please use latest Node.js v20 LTS for development.\x1b[0;0m'); err = true; } - if (majorNodeVersion >= 19) { - console.warn('\x1b[1;31m*** Warning: Versions of node.js >= 19 have not been tested.\x1b[0;0m') - } } const path = require('path'); diff --git a/build/package.json b/build/package.json index c9cd0af9f32..2b89bbc1c99 100644 --- a/build/package.json +++ b/build/package.json @@ -14,7 +14,7 @@ "@types/fancy-log": "^1.3.0", "@types/fs-extra": "^9.0.12", "@types/glob": "^7.1.1", - "@types/gulp": "^4.0.5", + "@types/gulp": "^4.0.17", "@types/gulp-concat": "^0.0.32", "@types/gulp-filter": "^3.0.32", "@types/gulp-gzip": "^0.0.31", @@ -26,7 +26,7 @@ "@types/minimist": "^1.2.1", "@types/mkdirp": "^1.0.1", "@types/mocha": "^9.1.1", - "@types/node": "18.x", + "@types/node": "20.x", "@types/pump": "^1.0.1", "@types/rimraf": "^2.0.4", "@types/through": "^0.0.29", diff --git a/build/tsconfig.build.json b/build/tsconfig.build.json index 801c7735b06..4534420208f 100644 --- a/build/tsconfig.build.json +++ b/build/tsconfig.build.json @@ -3,7 +3,8 @@ "compilerOptions": { "allowJs": false, "checkJs": false, - "noEmit": false + "noEmit": false, + "skipLibCheck": true }, "include": [ "**/*.ts" diff --git a/build/yarn.lock b/build/yarn.lock index cb4f5cfcae4..3131c43217c 100644 --- a/build/yarn.lock +++ b/build/yarn.lock @@ -404,14 +404,6 @@ "@types/node" "*" "@types/responselike" "^1.0.0" -"@types/chokidar@*": - version "1.7.5" - resolved "https://registry.yarnpkg.com/@types/chokidar/-/chokidar-1.7.5.tgz#1fa78c8803e035bed6d98e6949e514b133b0c9b6" - integrity sha512-PDkSRY7KltW3M60hSBlerxI8SFPXsO3AL/aRVsO4Kh9IHRW74Ih75gUuTd/aE4LSSFqypb10UIX3QzOJwBQMGQ== - dependencies: - "@types/events" "*" - "@types/node" "*" - "@types/debounce@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/debounce/-/debounce-1.0.0.tgz#417560200331e1bb84d72da85391102c2fcd61b7" @@ -508,14 +500,15 @@ dependencies: "@types/node" "*" -"@types/gulp@^4.0.5": - version "4.0.5" - resolved "https://registry.yarnpkg.com/@types/gulp/-/gulp-4.0.5.tgz#f5f498d5bf9538364792de22490a12c0e6bc5eb4" - integrity sha512-nx1QjPTiRpvLfYsZ7MBu7bT6Cm7tAXyLbY0xbdx2IEMxCK2v2urIhJMQZHW0iV1TskM71Xl6p2uRRuWDbk+/7g== +"@types/gulp@^4.0.17": + version "4.0.17" + resolved "https://registry.yarnpkg.com/@types/gulp/-/gulp-4.0.17.tgz#b314c3762d08d8d69b7c0b86f78d069bafd65009" + integrity sha512-+pKQynu2C/HS16kgmDlAicjtFYP8kaa86eE9P0Ae7GB5W29we/E2TIdbOWtEZD5XkpY+jr8fyqfwO6SWZecLpQ== dependencies: - "@types/chokidar" "*" - "@types/undertaker" "*" + "@types/node" "*" + "@types/undertaker" ">=1.2.6" "@types/vinyl-fs" "*" + chokidar "^3.3.1" "@types/http-cache-semantics@*": version "4.0.4" @@ -579,10 +572,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.51.tgz#b31d716fb8d58eeb95c068a039b9b6292817d5fb" integrity sha512-El3+WJk2D/ppWNd2X05aiP5l2k4EwF7KwheknQZls+I26eSICoWRhRIJ56jGgw2dqNGQ5LtNajmBU2ajS28EvQ== -"@types/node@18.x": - version "18.18.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.18.9.tgz#5527ea1832db3bba8eb8023ce8497b7d3f299592" - integrity sha512-0f5klcuImLnG4Qreu9hPj/rEfFq6YRc5n2mAjSsH+ec/mJL+3voBH0+8T7o8RpFjH7ovc+TRsL/c7OYIQsPTfQ== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== dependencies: undici-types "~5.26.4" @@ -639,13 +632,14 @@ resolved "https://registry.yarnpkg.com/@types/undertaker-registry/-/undertaker-registry-1.0.1.tgz#4306d4a03d7acedb974b66530832b90729e1d1da" integrity sha512-Z4TYuEKn9+RbNVk1Ll2SS4x1JeLHecolIbM/a8gveaHsW0Hr+RQMraZACwTO2VD7JvepgA6UO1A1VrbktQrIbQ== -"@types/undertaker@*": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@types/undertaker/-/undertaker-1.2.0.tgz#d39a81074b4f274eb656870fc904a70737e00f8e" - integrity sha512-bx/5nZCGkasXs6qaA3B6SVDjBZqdyk04UO12e0uEPSzjt5H8jEJw0DKe7O7IM0hM2bVHRh70pmOH7PEHqXwzOw== +"@types/undertaker@>=1.2.6": + version "1.2.11" + resolved "https://registry.yarnpkg.com/@types/undertaker/-/undertaker-1.2.11.tgz#d9e08b72c4bea5fc40e5bad63ad5a1a2b675e3ca" + integrity sha512-j1Z0V2ByRHr8ZK7eOeGq0LGkkdthNFW0uAZGY22iRkNQNL9/vAV0yFPr1QN3FM/peY5bxs9P+1f0PYJTQVa5iA== dependencies: - "@types/events" "*" + "@types/node" "*" "@types/undertaker-registry" "*" + async-done "~1.3.2" "@types/vinyl-fs@*": version "2.4.9" @@ -657,13 +651,6 @@ "@types/node" "*" "@types/vinyl" "*" -"@types/vinyl@*": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@types/vinyl/-/vinyl-2.0.2.tgz#4f3b8dae8f5828d3800ef709b0cff488ee852de3" - integrity sha512-2iYpNuOl98SrLPBZfEN9Mh2JCJ2EI9HU35SfgBEb51DcmaHkhp8cKMblYeBqMQiwXMgAD3W60DbQ4i/UdLiXhw== - dependencies: - "@types/node" "*" - "@types/vinyl@*": version "2.0.12" resolved "https://registry.yarnpkg.com/@types/vinyl/-/vinyl-2.0.12.tgz#17642ca9a8ae10f3db018e9f885da4188db4c6e6" @@ -776,6 +763,14 @@ anymatch@^3.1.1, anymatch@~3.1.1: normalize-path "^3.0.0" picomatch "^2.0.4" +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + argparse@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" @@ -808,6 +803,16 @@ assign-symbols@^1.0.0: resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= +async-done@~1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/async-done/-/async-done-1.3.2.tgz#5e15aa729962a4b07414f528a88cdf18e0b290a2" + integrity sha512-uYkTP8dw2og1tu1nmza1n1CMW0qb8gWWlwqMmLb7MhBVs4BXrFziT6HXUd+/RlRA/i4H9AkofYloUbs1fwMqlw== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.2" + process-nextick-args "^2.0.0" + stream-exhaust "^1.0.1" + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -1009,6 +1014,21 @@ chokidar@3.5.1: optionalDependencies: fsevents "~2.3.1" +chokidar@^3.3.1: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + chownr@^1.1.1: version "1.1.4" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" @@ -1473,6 +1493,11 @@ fsevents@~2.3.1: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" @@ -1499,7 +1524,7 @@ github-from-package@0.0.0: resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" integrity sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4= -glob-parent@^5.1.1, glob-parent@~5.1.0: +glob-parent@^5.1.1, glob-parent@~5.1.0, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== @@ -2038,11 +2063,6 @@ mute-stream@~0.0.4: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== -nan@^2.17.0: - version "2.18.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.18.0.tgz#26a6faae7ffbeb293a39660e88a76b82e30b7554" - integrity sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w== - nan@^2.18.0: version "2.19.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.19.0.tgz#bb58122ad55a6c5bc973303908d5b16cfdd5a8c0" @@ -2109,7 +2129,7 @@ object-keys@^1.0.12: resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== -once@^1.3.0, once@^1.3.1, once@^1.4.0: +once@^1.3.0, once@^1.3.1, once@^1.3.2, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= @@ -2205,7 +2225,7 @@ plugin-error@1.0.1, plugin-error@^1.0.1: arr-union "^3.1.0" extend-shallow "^3.0.2" -prebuild-install@^7.0.1, prebuild-install@^7.1.1: +prebuild-install@^7.0.1: version "7.1.1" resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45" integrity sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw== @@ -2223,6 +2243,24 @@ prebuild-install@^7.0.1, prebuild-install@^7.1.1: tar-fs "^2.0.0" tunnel-agent "^0.6.0" +prebuild-install@^7.1.1: + version "7.1.2" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.2.tgz#a5fd9986f5a6251fbc47e1e5c65de71e68c0a056" + integrity sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ== + dependencies: + detect-libc "^2.0.0" + expand-template "^2.0.3" + github-from-package "0.0.0" + minimist "^1.2.3" + mkdirp-classic "^0.5.3" + napi-build-utils "^1.0.1" + node-abi "^3.3.0" + pump "^3.0.0" + rc "^1.2.7" + simple-get "^4.0.0" + tar-fs "^2.0.0" + tunnel-agent "^0.6.0" + priorityqueuejs@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/priorityqueuejs/-/priorityqueuejs-1.0.0.tgz#2ee4f23c2560913e08c07ce5ccdd6de3df2c5af8" @@ -2309,6 +2347,13 @@ readdirp@~3.5.0: dependencies: picomatch "^2.2.1" +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + remove-trailing-separator@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" @@ -2449,6 +2494,11 @@ stoppable@^1.1.0: resolved "https://registry.yarnpkg.com/stoppable/-/stoppable-1.1.0.tgz#32da568e83ea488b08e4d7ea2c3bcc9d75015d5b" integrity sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw== +stream-exhaust@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/stream-exhaust/-/stream-exhaust-1.0.2.tgz#acdac8da59ef2bc1e17a2c0ccf6c320d120e555d" + integrity sha512-b/qaq/GlBK5xaq1yrK9/zFcyRSTNxmcZwFLGSTG0mXgZl/4Z6GgiyYOXOvY7N3eEvFRAG1bkDRz5EPGSvPYQlw== + stream-shift@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" @@ -2599,15 +2649,7 @@ tree-sitter-typescript@^0.20.5: nan "^2.18.0" tree-sitter "^0.20.6" -tree-sitter@^0.20.5: - version "0.20.5" - resolved "https://registry.yarnpkg.com/tree-sitter/-/tree-sitter-0.20.5.tgz#554741ee06b984824dd5082353aa2a28bcefa271" - integrity sha512-xjxkKCKV7F2F5HWmyRE4bosoxkbxe9lYvFRc/nzmtHNqFNTwYwh0oWVVEt0VnbupZHMirEQW7vDx8ddJn72tjg== - dependencies: - nan "^2.17.0" - prebuild-install "^7.1.1" - -tree-sitter@^0.20.6: +tree-sitter@^0.20.5, tree-sitter@^0.20.6: version "0.20.6" resolved "https://registry.yarnpkg.com/tree-sitter/-/tree-sitter-0.20.6.tgz#fec52e5d7cc6c583135756479f2440dd89b25cbe" integrity sha512-GxJodajVpfgb3UREzzIbtA1hyRnTxVbWVXrbC6sk4xTMH5ERMBJk9HJNq4c8jOJeUaIOmLcwg+t6mez/PDvGqg== diff --git a/cgmanifest.json b/cgmanifest.json index 891a0b0cb32..a85c770cff2 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "chromium", "repositoryUrl": "https://chromium.googlesource.com/chromium/src", - "commitHash": "14d11e5bb9b5b1cd51f7b19546e74a73cab42084" + "commitHash": "f1a45d7ded05d64ca8136cc142ddc0c271b1dd43" } }, "licenseDetail": [ @@ -40,7 +40,7 @@ "SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ], "isOnlyProductionDependency": true, - "version": "120.0.6099.291" + "version": "122.0.6261.156" }, { "component": { @@ -48,7 +48,7 @@ "git": { "name": "ffmpeg", "repositoryUrl": "https://chromium.googlesource.com/chromium/third_party/ffmpeg", - "commitHash": "e1ca3f06adec15150a171bc38f550058b4bbb23b" + "commitHash": "17525de887d54b970ffdd421a0879c1db1952307" } }, "isOnlyProductionDependency": true, @@ -516,11 +516,11 @@ "git": { "name": "nodejs", "repositoryUrl": "https://github.com/nodejs/node", - "commitHash": "8a01b3dcb7d08a48bfd3e6bf85ef49faa1454839" + "commitHash": "22f383dcd529d6bf790856db614a35fea78e825f" } }, "isOnlyProductionDependency": true, - "version": "18.18.2" + "version": "20.9.0" }, { "component": { @@ -528,12 +528,12 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "31cd9d1f61714e20f1067d726404600ab7281698" + "commitHash": "384642792eb521b978a008ee1dbc30885edb7dcb" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "28.2.8" + "version": "29.3.1" }, { "component": { diff --git a/extensions/configuration-editing/package.json b/extensions/configuration-editing/package.json index b80a187e266..8a00fda49b8 100644 --- a/extensions/configuration-editing/package.json +++ b/extensions/configuration-editing/package.json @@ -161,7 +161,7 @@ ] }, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "repository": { "type": "git", diff --git a/extensions/configuration-editing/yarn.lock b/extensions/configuration-editing/yarn.lock index 7672e88e7a4..0e7d733c76f 100644 --- a/extensions/configuration-editing/yarn.lock +++ b/extensions/configuration-editing/yarn.lock @@ -125,10 +125,12 @@ dependencies: "@octokit/openapi-types" "^18.0.0" -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" before-after-hook@^2.2.0: version "2.2.3" @@ -174,6 +176,11 @@ tunnel@^0.0.6: resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + universal-user-agent@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee" diff --git a/extensions/css-language-features/package.json b/extensions/css-language-features/package.json index a0c1107984c..f4f6adfb7f4 100644 --- a/extensions/css-language-features/package.json +++ b/extensions/css-language-features/package.json @@ -998,7 +998,7 @@ "vscode-uri": "^3.0.8" }, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "repository": { "type": "git", diff --git a/extensions/css-language-features/server/package.json b/extensions/css-language-features/server/package.json index 1b1707150e6..0f1750e800a 100644 --- a/extensions/css-language-features/server/package.json +++ b/extensions/css-language-features/server/package.json @@ -17,7 +17,7 @@ }, "devDependencies": { "@types/mocha": "^9.1.1", - "@types/node": "18.x" + "@types/node": "20.x" }, "scripts": { "compile": "gulp compile-extension:css-language-features-server", diff --git a/extensions/css-language-features/server/yarn.lock b/extensions/css-language-features/server/yarn.lock index a30d56158e5..8d4c46d641e 100644 --- a/extensions/css-language-features/server/yarn.lock +++ b/extensions/css-language-features/server/yarn.lock @@ -7,10 +7,10 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.1.1.tgz#e7c4f1001eefa4b8afbd1eee27a237fee3bf29c4" integrity sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw== -"@types/node@18.x": - version "18.19.8" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.8.tgz#c1e42b165e5a526caf1f010747e0522cb2c9c36a" - integrity sha512-g1pZtPhsvGVTwmeVoexWZLTQaOvXwoSq//pTL0DHeNzUDrFnir4fgETdhjhIxjVnN+hKOuh98+E1eMLnUXstFg== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== dependencies: undici-types "~5.26.4" diff --git a/extensions/css-language-features/yarn.lock b/extensions/css-language-features/yarn.lock index b8e23eb26ce..25a22d07ca6 100644 --- a/extensions/css-language-features/yarn.lock +++ b/extensions/css-language-features/yarn.lock @@ -2,10 +2,12 @@ # yarn lockfile v1 -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.npmjs.org/@types/node/-/node-18.15.13.tgz" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" balanced-match@^1.0.0: version "1.0.2" @@ -40,6 +42,11 @@ semver@^7.6.0: dependencies: lru-cache "^6.0.0" +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + vscode-jsonrpc@9.0.0-next.2: version "9.0.0-next.2" resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.2.tgz#29e9741c742c80329bba1c60ce38fd014651ba80" diff --git a/extensions/debug-auto-launch/package.json b/extensions/debug-auto-launch/package.json index 0bc095522a0..4a5d3361f95 100644 --- a/extensions/debug-auto-launch/package.json +++ b/extensions/debug-auto-launch/package.json @@ -33,7 +33,7 @@ ] }, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "prettier": { "printWidth": 100, diff --git a/extensions/debug-auto-launch/yarn.lock b/extensions/debug-auto-launch/yarn.lock index 8a3d10f2b65..1f4b6c2e8b4 100644 --- a/extensions/debug-auto-launch/yarn.lock +++ b/extensions/debug-auto-launch/yarn.lock @@ -2,7 +2,14 @@ # yarn lockfile v1 -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== diff --git a/extensions/debug-server-ready/package.json b/extensions/debug-server-ready/package.json index 65efcdc09f3..2afe977a9fc 100644 --- a/extensions/debug-server-ready/package.json +++ b/extensions/debug-server-ready/package.json @@ -212,7 +212,7 @@ ] }, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "repository": { "type": "git", diff --git a/extensions/debug-server-ready/yarn.lock b/extensions/debug-server-ready/yarn.lock index 8a3d10f2b65..1f4b6c2e8b4 100644 --- a/extensions/debug-server-ready/yarn.lock +++ b/extensions/debug-server-ready/yarn.lock @@ -2,7 +2,14 @@ # yarn lockfile v1 -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== diff --git a/extensions/emmet/package.json b/extensions/emmet/package.json index b7291b9552f..1783bc2ceaf 100644 --- a/extensions/emmet/package.json +++ b/extensions/emmet/package.json @@ -479,7 +479,7 @@ "deps": "yarn add @vscode/emmet-helper" }, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "dependencies": { "@emmetio/css-parser": "ramya-rao-a/css-parser#vscode", diff --git a/extensions/emmet/yarn.lock b/extensions/emmet/yarn.lock index c6d7b12db0e..b75842fe4a4 100644 --- a/extensions/emmet/yarn.lock +++ b/extensions/emmet/yarn.lock @@ -53,10 +53,12 @@ resolved "https://registry.yarnpkg.com/@emmetio/stream-reader/-/stream-reader-2.2.0.tgz#46cffea119a0a003312a21c2d9b5628cb5fcd442" integrity sha1-Rs/+oRmgoAMxKiHC2bVijLX81EI= -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@vscode/emmet-helper@^2.8.8": version "2.9.3" @@ -101,6 +103,11 @@ queue@6.0.2: dependencies: inherits "~2.0.3" +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + vscode-languageserver-textdocument@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.3.tgz#879f2649bfa5a6e07bc8b392c23ede2dfbf43eff" diff --git a/extensions/extension-editing/package.json b/extensions/extension-editing/package.json index f45105b99d4..184d28e8df0 100644 --- a/extensions/extension-editing/package.json +++ b/extensions/extension-editing/package.json @@ -67,7 +67,7 @@ }, "devDependencies": { "@types/markdown-it": "0.0.2", - "@types/node": "18.x" + "@types/node": "20.x" }, "repository": { "type": "git", diff --git a/extensions/extension-editing/src/extensionLinter.ts b/extensions/extension-editing/src/extensionLinter.ts index 8bb2a4640df..dd1727edb7b 100644 --- a/extensions/extension-editing/src/extensionLinter.ts +++ b/extensions/extension-editing/src/extensionLinter.ts @@ -66,7 +66,7 @@ export class ExtensionLinter { private folderToPackageJsonInfo: Record = {}; private packageJsonQ = new Set(); private readmeQ = new Set(); - private timer: NodeJS.Timer | undefined; + private timer: NodeJS.Timeout | undefined; private markdownIt: MarkdownItType.MarkdownIt | undefined; private parse5: typeof import('parse5') | undefined; diff --git a/extensions/extension-editing/yarn.lock b/extensions/extension-editing/yarn.lock index 5456b3ec040..00fad585fd1 100644 --- a/extensions/extension-editing/yarn.lock +++ b/extensions/extension-editing/yarn.lock @@ -7,10 +7,12 @@ resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-0.0.2.tgz#5d9ad19e6e6508cdd2f2596df86fd0aade598660" integrity sha1-XZrRnm5lCM3S8llt+G/Qqt5ZhmA= -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@types/node@^6.0.46": version "6.0.78" @@ -66,3 +68,8 @@ uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== diff --git a/extensions/git-base/package.json b/extensions/git-base/package.json index c65a8f47e61..3c9b07a13e8 100644 --- a/extensions/git-base/package.json +++ b/extensions/git-base/package.json @@ -104,7 +104,7 @@ ] }, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "repository": { "type": "git", diff --git a/extensions/git-base/yarn.lock b/extensions/git-base/yarn.lock index 8a3d10f2b65..1f4b6c2e8b4 100644 --- a/extensions/git-base/yarn.lock +++ b/extensions/git-base/yarn.lock @@ -2,7 +2,14 @@ # yarn lockfile v1 -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== diff --git a/extensions/git/package.json b/extensions/git/package.json index 74e09578256..dfbb29289db 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -3409,7 +3409,7 @@ "devDependencies": { "@types/byline": "4.2.31", "@types/mocha": "^9.1.1", - "@types/node": "18.x", + "@types/node": "20.x", "@types/picomatch": "2.3.0", "@types/which": "3.0.0" }, diff --git a/extensions/git/src/util.ts b/extensions/git/src/util.ts index 219c87b148d..eac6f0384b9 100644 --- a/extensions/git/src/util.ts +++ b/extensions/git/src/util.ts @@ -81,7 +81,7 @@ export function onceEvent(event: Event): Event { export function debounceEvent(event: Event, delay: number): Event { return (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) => { - let timer: NodeJS.Timer; + let timer: NodeJS.Timeout; return event(e => { clearTimeout(timer); timer = setTimeout(() => listener.call(thisArgs, e), delay); diff --git a/extensions/git/yarn.lock b/extensions/git/yarn.lock index dfb24f6e7d2..266157e9e5c 100644 --- a/extensions/git/yarn.lock +++ b/extensions/git/yarn.lock @@ -122,10 +122,12 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.51.tgz#b31d716fb8d58eeb95c068a039b9b6292817d5fb" integrity sha512-El3+WJk2D/ppWNd2X05aiP5l2k4EwF7KwheknQZls+I26eSICoWRhRIJ56jGgw2dqNGQ5LtNajmBU2ajS28EvQ== -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@types/picomatch@2.3.0": version "2.3.0" @@ -239,6 +241,11 @@ token-types@^4.1.1: "@tokenizer/token" "^0.3.0" ieee754 "^1.2.1" +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + util-deprecate@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" diff --git a/extensions/github-authentication/package.json b/extensions/github-authentication/package.json index d55e8dcfd03..2d2bea56277 100644 --- a/extensions/github-authentication/package.json +++ b/extensions/github-authentication/package.json @@ -65,7 +65,7 @@ }, "devDependencies": { "@types/mocha": "^9.1.1", - "@types/node": "18.x", + "@types/node": "20.x", "@types/node-fetch": "^2.5.7" }, "repository": { diff --git a/extensions/github-authentication/yarn.lock b/extensions/github-authentication/yarn.lock index 724b304c53e..8ef2192404a 100644 --- a/extensions/github-authentication/yarn.lock +++ b/extensions/github-authentication/yarn.lock @@ -113,10 +113,12 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.5.tgz#3d03acd3b3414cf67faf999aed11682ed121f22b" integrity sha512-90hiq6/VqtQgX8Sp0EzeIsv3r+ellbGj4URKj5j30tLlZvRUpnAe9YbYnjl3pJM93GyXU0tghHhvXHq+5rnCKA== -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@vscode/extension-telemetry@^0.9.0": version "0.9.0" @@ -182,6 +184,11 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + vscode-tas-client@^0.1.84: version "0.1.84" resolved "https://registry.yarnpkg.com/vscode-tas-client/-/vscode-tas-client-0.1.84.tgz#906bdcfd8c9e1dc04321d6bc0335184f9119968e" diff --git a/extensions/github/package.json b/extensions/github/package.json index 5596a2c0557..ece19e32f54 100644 --- a/extensions/github/package.json +++ b/extensions/github/package.json @@ -186,7 +186,7 @@ "@vscode/extension-telemetry": "^0.9.0" }, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "repository": { "type": "git", diff --git a/extensions/github/yarn.lock b/extensions/github/yarn.lock index caf35ace98a..912a28439be 100644 --- a/extensions/github/yarn.lock +++ b/extensions/github/yarn.lock @@ -225,10 +225,12 @@ dependencies: "@octokit/openapi-types" "^17.1.0" -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@vscode/extension-telemetry@^0.9.0": version "0.9.0" @@ -295,6 +297,11 @@ tunnel@^0.0.6: resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + universal-user-agent@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee" diff --git a/extensions/grunt/package.json b/extensions/grunt/package.json index 6869f9ce506..ae533cc0e47 100644 --- a/extensions/grunt/package.json +++ b/extensions/grunt/package.json @@ -19,7 +19,7 @@ }, "dependencies": {}, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "main": "./out/main", "activationEvents": [ diff --git a/extensions/grunt/yarn.lock b/extensions/grunt/yarn.lock index 8a3d10f2b65..1f4b6c2e8b4 100644 --- a/extensions/grunt/yarn.lock +++ b/extensions/grunt/yarn.lock @@ -2,7 +2,14 @@ # yarn lockfile v1 -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== diff --git a/extensions/gulp/package.json b/extensions/gulp/package.json index 3e29c75fe4d..0c19b688477 100644 --- a/extensions/gulp/package.json +++ b/extensions/gulp/package.json @@ -18,7 +18,7 @@ }, "dependencies": {}, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "main": "./out/main", "activationEvents": [ diff --git a/extensions/gulp/yarn.lock b/extensions/gulp/yarn.lock index 8a3d10f2b65..1f4b6c2e8b4 100644 --- a/extensions/gulp/yarn.lock +++ b/extensions/gulp/yarn.lock @@ -2,7 +2,14 @@ # yarn lockfile v1 -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== diff --git a/extensions/html-language-features/package.json b/extensions/html-language-features/package.json index dd984454949..49489ff20df 100644 --- a/extensions/html-language-features/package.json +++ b/extensions/html-language-features/package.json @@ -263,7 +263,7 @@ "vscode-uri": "^3.0.8" }, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "repository": { "type": "git", diff --git a/extensions/html-language-features/server/package.json b/extensions/html-language-features/server/package.json index 0e3ec8667ab..75bfa00de11 100644 --- a/extensions/html-language-features/server/package.json +++ b/extensions/html-language-features/server/package.json @@ -18,7 +18,7 @@ }, "devDependencies": { "@types/mocha": "^9.1.1", - "@types/node": "18.x" + "@types/node": "20.x" }, "scripts": { "compile": "npx gulp compile-extension:html-language-features-server", diff --git a/extensions/html-language-features/server/src/languageModelCache.ts b/extensions/html-language-features/server/src/languageModelCache.ts index 048d84d37cd..5dd8e439f5c 100644 --- a/extensions/html-language-features/server/src/languageModelCache.ts +++ b/extensions/html-language-features/server/src/languageModelCache.ts @@ -15,7 +15,7 @@ export function getLanguageModelCache(maxEntries: number, cleanupIntervalTime let languageModels: { [uri: string]: { version: number; languageId: string; cTime: number; languageModel: T } } = {}; let nModels = 0; - let cleanupInterval: NodeJS.Timer | undefined = undefined; + let cleanupInterval: NodeJS.Timeout | undefined = undefined; if (cleanupIntervalTimeInSec > 0) { cleanupInterval = setInterval(() => { const cutoffTime = Date.now() - cleanupIntervalTimeInSec * 1000; diff --git a/extensions/html-language-features/server/yarn.lock b/extensions/html-language-features/server/yarn.lock index c8ba196769b..f327f1f352f 100644 --- a/extensions/html-language-features/server/yarn.lock +++ b/extensions/html-language-features/server/yarn.lock @@ -7,16 +7,23 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.1.1.tgz#e7c4f1001eefa4b8afbd1eee27a237fee3bf29c4" integrity sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw== -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.12.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.5.tgz#74c4f31ab17955d0b5808cdc8fd2839526ad00b3" + integrity sha512-BD+BjQ9LS/D8ST9p5uqBxghlN+S42iuNxjsUGjeZobe/ciXzk2qb1B6IXc6AnRLS+yFJRpN2IPEHMzwspfDJNw== + dependencies: + undici-types "~5.26.4" "@vscode/l10n@^0.0.18": version "0.0.18" resolved "https://registry.yarnpkg.com/@vscode/l10n/-/l10n-0.0.18.tgz#916d3a5e960dbab47c1c56f58a7cb5087b135c95" integrity sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ== +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + vscode-css-languageservice@^6.2.13: version "6.2.13" resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-6.2.13.tgz#c7c2dc7a081a203048d60157c65536767d6d96f8" diff --git a/extensions/html-language-features/yarn.lock b/extensions/html-language-features/yarn.lock index b8072052af1..d1d73407809 100644 --- a/extensions/html-language-features/yarn.lock +++ b/extensions/html-language-features/yarn.lock @@ -95,10 +95,12 @@ resolved "https://registry.yarnpkg.com/@nevware21/ts-utils/-/ts-utils-0.10.1.tgz#aa65abc71eba06749a396598f22263d26f796ac7" integrity sha512-pMny25NnF2/MJwdqC3Iyjm2pGIXNxni4AROpcqDeWa+td9JMUY4bUS9uU9XW+BoBRqTLUL+WURF9SOd/6OQzRg== -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@vscode/extension-telemetry@^0.9.0": version "0.9.0" @@ -142,6 +144,11 @@ semver@^7.6.0: dependencies: lru-cache "^6.0.0" +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + vscode-jsonrpc@9.0.0-next.2: version "9.0.0-next.2" resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.2.tgz#29e9741c742c80329bba1c60ce38fd014651ba80" diff --git a/extensions/jake/package.json b/extensions/jake/package.json index 637d417e503..1d5d1250db0 100644 --- a/extensions/jake/package.json +++ b/extensions/jake/package.json @@ -18,7 +18,7 @@ }, "dependencies": {}, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "main": "./out/main", "activationEvents": [ diff --git a/extensions/jake/yarn.lock b/extensions/jake/yarn.lock index 8a3d10f2b65..1f4b6c2e8b4 100644 --- a/extensions/jake/yarn.lock +++ b/extensions/jake/yarn.lock @@ -2,7 +2,14 @@ # yarn lockfile v1 -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== diff --git a/extensions/json-language-features/package.json b/extensions/json-language-features/package.json index 7df9aa20be8..f86470429a4 100644 --- a/extensions/json-language-features/package.json +++ b/extensions/json-language-features/package.json @@ -166,7 +166,7 @@ "vscode-languageclient": "^10.0.0-next.5" }, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "repository": { "type": "git", diff --git a/extensions/json-language-features/server/package.json b/extensions/json-language-features/server/package.json index 46e8f0d94dd..6134fb4224d 100644 --- a/extensions/json-language-features/server/package.json +++ b/extensions/json-language-features/server/package.json @@ -21,7 +21,7 @@ }, "devDependencies": { "@types/mocha": "^9.1.1", - "@types/node": "18.x" + "@types/node": "20.x" }, "scripts": { "prepublishOnly": "npm run clean && npm run compile", diff --git a/extensions/json-language-features/server/src/languageModelCache.ts b/extensions/json-language-features/server/src/languageModelCache.ts index 17ffe2add4f..441a5a19b28 100644 --- a/extensions/json-language-features/server/src/languageModelCache.ts +++ b/extensions/json-language-features/server/src/languageModelCache.ts @@ -15,7 +15,7 @@ export function getLanguageModelCache(maxEntries: number, cleanupIntervalTime let languageModels: { [uri: string]: { version: number; languageId: string; cTime: number; languageModel: T } } = {}; let nModels = 0; - let cleanupInterval: NodeJS.Timer | undefined = undefined; + let cleanupInterval: NodeJS.Timeout | undefined = undefined; if (cleanupIntervalTimeInSec > 0) { cleanupInterval = setInterval(() => { const cutoffTime = Date.now() - cleanupIntervalTimeInSec * 1000; @@ -79,4 +79,4 @@ export function getLanguageModelCache(maxEntries: number, cleanupIntervalTime } } }; -} \ No newline at end of file +} diff --git a/extensions/json-language-features/server/yarn.lock b/extensions/json-language-features/server/yarn.lock index 343feaa178f..669e823497d 100644 --- a/extensions/json-language-features/server/yarn.lock +++ b/extensions/json-language-features/server/yarn.lock @@ -7,10 +7,12 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.1.1.tgz#e7c4f1001eefa4b8afbd1eee27a237fee3bf29c4" integrity sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw== -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.12.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.5.tgz#74c4f31ab17955d0b5808cdc8fd2839526ad00b3" + integrity sha512-BD+BjQ9LS/D8ST9p5uqBxghlN+S42iuNxjsUGjeZobe/ciXzk2qb1B6IXc6AnRLS+yFJRpN2IPEHMzwspfDJNw== + dependencies: + undici-types "~5.26.4" "@vscode/l10n@^0.0.18": version "0.0.18" @@ -27,6 +29,11 @@ request-light@^0.7.0: resolved "https://registry.yarnpkg.com/request-light/-/request-light-0.7.0.tgz#885628bb2f8040c26401ebf258ec51c4ae98ac2a" integrity sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q== +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + vscode-json-languageservice@^5.3.11: version "5.3.11" resolved "https://registry.yarnpkg.com/vscode-json-languageservice/-/vscode-json-languageservice-5.3.11.tgz#71dbc56e9b1d07a57aa6a3d5569c8b7f2c05ca05" diff --git a/extensions/json-language-features/yarn.lock b/extensions/json-language-features/yarn.lock index 8374cca5b83..b7ca937103a 100644 --- a/extensions/json-language-features/yarn.lock +++ b/extensions/json-language-features/yarn.lock @@ -95,10 +95,12 @@ resolved "https://registry.yarnpkg.com/@nevware21/ts-utils/-/ts-utils-0.10.1.tgz#aa65abc71eba06749a396598f22263d26f796ac7" integrity sha512-pMny25NnF2/MJwdqC3Iyjm2pGIXNxni4AROpcqDeWa+td9JMUY4bUS9uU9XW+BoBRqTLUL+WURF9SOd/6OQzRg== -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@vscode/extension-telemetry@^0.9.0": version "0.9.0" @@ -147,6 +149,11 @@ semver@^7.6.0: dependencies: lru-cache "^6.0.0" +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + vscode-jsonrpc@9.0.0-next.2: version "9.0.0-next.2" resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.2.tgz#29e9741c742c80329bba1c60ce38fd014651ba80" diff --git a/extensions/markdown-language-features/server/package.json b/extensions/markdown-language-features/server/package.json index df9124c936b..532c2dec843 100644 --- a/extensions/markdown-language-features/server/package.json +++ b/extensions/markdown-language-features/server/package.json @@ -22,7 +22,7 @@ "vscode-uri": "^3.0.7" }, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "scripts": { "compile": "gulp compile-extension:markdown-language-features-server", diff --git a/extensions/markdown-language-features/server/yarn.lock b/extensions/markdown-language-features/server/yarn.lock index 0768663fe0d..148783435bd 100644 --- a/extensions/markdown-language-features/server/yarn.lock +++ b/extensions/markdown-language-features/server/yarn.lock @@ -2,10 +2,12 @@ # yarn lockfile v1 -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@vscode/l10n@^0.0.10": version "0.0.10" @@ -98,6 +100,11 @@ picomatch@^2.3.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + vscode-jsonrpc@8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.1.0.tgz#cb9989c65e219e18533cc38e767611272d274c94" diff --git a/extensions/merge-conflict/package.json b/extensions/merge-conflict/package.json index f55fb26c44e..cdda46fab32 100644 --- a/extensions/merge-conflict/package.json +++ b/extensions/merge-conflict/package.json @@ -169,7 +169,7 @@ "@vscode/extension-telemetry": "^0.9.0" }, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "repository": { "type": "git", diff --git a/extensions/merge-conflict/yarn.lock b/extensions/merge-conflict/yarn.lock index f93736b6b27..31f7cee0830 100644 --- a/extensions/merge-conflict/yarn.lock +++ b/extensions/merge-conflict/yarn.lock @@ -95,10 +95,12 @@ resolved "https://registry.yarnpkg.com/@nevware21/ts-utils/-/ts-utils-0.10.1.tgz#aa65abc71eba06749a396598f22263d26f796ac7" integrity sha512-pMny25NnF2/MJwdqC3Iyjm2pGIXNxni4AROpcqDeWa+td9JMUY4bUS9uU9XW+BoBRqTLUL+WURF9SOd/6OQzRg== -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@vscode/extension-telemetry@^0.9.0": version "0.9.0" @@ -108,3 +110,8 @@ "@microsoft/1ds-core-js" "^4.0.3" "@microsoft/1ds-post-js" "^4.0.3" "@microsoft/applicationinsights-web-basic" "^3.0.4" + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== diff --git a/extensions/microsoft-authentication/package.json b/extensions/microsoft-authentication/package.json index c82eea19318..3d73a7621fd 100644 --- a/extensions/microsoft-authentication/package.json +++ b/extensions/microsoft-authentication/package.json @@ -109,7 +109,7 @@ "watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose" }, "devDependencies": { - "@types/node": "18.x", + "@types/node": "20.x", "@types/node-fetch": "^2.5.7", "@types/randombytes": "^2.0.0", "@types/sha.js": "^2.4.0", diff --git a/extensions/microsoft-authentication/yarn.lock b/extensions/microsoft-authentication/yarn.lock index afa82e5759f..6f277110a56 100644 --- a/extensions/microsoft-authentication/yarn.lock +++ b/extensions/microsoft-authentication/yarn.lock @@ -113,10 +113,12 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.23.tgz#676fa0883450ed9da0bb24156213636290892806" integrity sha512-Z4U8yDAl5TFkmYsZdFPdjeMa57NOvnaf1tljHzhouaPEp7LCj2JKkejpI1ODviIAQuW4CcQmxkQ77rnLsOOoKw== -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@types/randombytes@^2.0.0": version "2.0.0" @@ -196,6 +198,11 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" diff --git a/extensions/npm/package.json b/extensions/npm/package.json index 37411d32e52..545ce102ab5 100644 --- a/extensions/npm/package.json +++ b/extensions/npm/package.json @@ -32,7 +32,7 @@ }, "devDependencies": { "@types/minimatch": "^5.1.2", - "@types/node": "18.x", + "@types/node": "20.x", "@types/which": "^3.0.0" }, "main": "./out/npmMain", diff --git a/extensions/npm/yarn.lock b/extensions/npm/yarn.lock index 7dad0575479..a7afc9f801f 100644 --- a/extensions/npm/yarn.lock +++ b/extensions/npm/yarn.lock @@ -7,10 +7,12 @@ resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca" integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA== -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@types/which@^3.0.0": version "3.0.0" @@ -181,6 +183,11 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + vscode-uri@^3.0.8: version "3.0.8" resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.8.tgz#1770938d3e72588659a172d0fd4642780083ff9f" diff --git a/extensions/php-language-features/package.json b/extensions/php-language-features/package.json index 8963fc796f4..989213b6c0c 100644 --- a/extensions/php-language-features/package.json +++ b/extensions/php-language-features/package.json @@ -77,7 +77,7 @@ "which": "^2.0.2" }, "devDependencies": { - "@types/node": "18.x", + "@types/node": "20.x", "@types/which": "^2.0.0" }, "repository": { diff --git a/extensions/php-language-features/yarn.lock b/extensions/php-language-features/yarn.lock index 4c2e01e4b71..ea9947b69e3 100644 --- a/extensions/php-language-features/yarn.lock +++ b/extensions/php-language-features/yarn.lock @@ -2,10 +2,12 @@ # yarn lockfile v1 -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@types/which@^2.0.0": version "2.0.0" @@ -17,6 +19,11 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + which@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" diff --git a/extensions/references-view/package.json b/extensions/references-view/package.json index 228332773c6..9566a965c76 100644 --- a/extensions/references-view/package.json +++ b/extensions/references-view/package.json @@ -399,6 +399,6 @@ "watch": "npx gulp watch-extension:references-view" }, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" } } diff --git a/extensions/references-view/yarn.lock b/extensions/references-view/yarn.lock index 8a3d10f2b65..1f4b6c2e8b4 100644 --- a/extensions/references-view/yarn.lock +++ b/extensions/references-view/yarn.lock @@ -2,7 +2,14 @@ # yarn lockfile v1 -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== diff --git a/extensions/tunnel-forwarding/package.json b/extensions/tunnel-forwarding/package.json index 76c61e4dad9..315baa03598 100644 --- a/extensions/tunnel-forwarding/package.json +++ b/extensions/tunnel-forwarding/package.json @@ -44,7 +44,7 @@ "watch": "gulp watch-extension:tunnel-forwarding" }, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "prettier": { "printWidth": 100, diff --git a/extensions/tunnel-forwarding/yarn.lock b/extensions/tunnel-forwarding/yarn.lock index 8a3d10f2b65..1f4b6c2e8b4 100644 --- a/extensions/tunnel-forwarding/yarn.lock +++ b/extensions/tunnel-forwarding/yarn.lock @@ -2,7 +2,14 @@ # yarn lockfile v1 -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index 45c687225b5..53aca1c8562 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -50,7 +50,7 @@ "vscode-uri": "^3.0.3" }, "devDependencies": { - "@types/node": "18.x", + "@types/node": "20.x", "@types/semver": "^5.5.0" }, "scripts": { diff --git a/extensions/typescript-language-features/src/languageFeatures/diagnostics.ts b/extensions/typescript-language-features/src/languageFeatures/diagnostics.ts index 450ffcb886c..190e6a99bf7 100644 --- a/extensions/typescript-language-features/src/languageFeatures/diagnostics.ts +++ b/extensions/typescript-language-features/src/languageFeatures/diagnostics.ts @@ -156,7 +156,7 @@ class DiagnosticsTelemetryManager extends Disposable { private readonly _diagnosticCodesMap = new Map(); private readonly _diagnosticSnapshotsMap = new ResourceMap(uri => uri.toString(), { onCaseInsensitiveFileSystem: false }); private _timeout: NodeJS.Timeout | undefined; - private _telemetryEmitter: NodeJS.Timer | undefined; + private _telemetryEmitter: NodeJS.Timeout | undefined; constructor( private readonly _telemetryReporter: TelemetryReporter, diff --git a/extensions/typescript-language-features/src/languageFeatures/tagClosing.ts b/extensions/typescript-language-features/src/languageFeatures/tagClosing.ts index 36c894e8e1e..45ac08e14a4 100644 --- a/extensions/typescript-language-features/src/languageFeatures/tagClosing.ts +++ b/extensions/typescript-language-features/src/languageFeatures/tagClosing.ts @@ -17,7 +17,7 @@ class TagClosing extends Disposable { public static readonly minVersion = API.v300; private _disposed = false; - private _timeout: NodeJS.Timer | undefined = undefined; + private _timeout: NodeJS.Timeout | undefined = undefined; private _cancel: vscode.CancellationTokenSource | undefined = undefined; constructor( diff --git a/extensions/typescript-language-features/src/ui/typingsStatus.ts b/extensions/typescript-language-features/src/ui/typingsStatus.ts index 2e0d53b363e..3e8d7c4efac 100644 --- a/extensions/typescript-language-features/src/ui/typingsStatus.ts +++ b/extensions/typescript-language-features/src/ui/typingsStatus.ts @@ -11,7 +11,7 @@ import { Disposable } from '../utils/dispose'; const typingsInstallTimeout = 30 * 1000; export default class TypingsStatus extends Disposable { - private readonly _acquiringTypings = new Map(); + private readonly _acquiringTypings = new Map(); private readonly _client: ITypeScriptServiceClient; constructor(client: ITypeScriptServiceClient) { diff --git a/extensions/typescript-language-features/yarn.lock b/extensions/typescript-language-features/yarn.lock index bc72fe4cb8b..e43e95500ce 100644 --- a/extensions/typescript-language-features/yarn.lock +++ b/extensions/typescript-language-features/yarn.lock @@ -95,10 +95,12 @@ resolved "https://registry.yarnpkg.com/@nevware21/ts-utils/-/ts-utils-0.10.1.tgz#aa65abc71eba06749a396598f22263d26f796ac7" integrity sha512-pMny25NnF2/MJwdqC3Iyjm2pGIXNxni4AROpcqDeWa+td9JMUY4bUS9uU9XW+BoBRqTLUL+WURF9SOd/6OQzRg== -"@types/node@18.x": - version "18.17.11" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.17.11.tgz#c04054659d88bfeba94095f41ef99a8ddf4e1813" - integrity sha512-r3hjHPBu+3LzbGBa8DHnr/KAeTEEOrahkcL+cZc4MaBMTM+mk8LtXR+zw+nqfjuDZZzYTYgTcpHuP+BEQk069g== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@types/semver@^5.5.0": version "5.5.0" @@ -164,6 +166,11 @@ tas-client@0.2.33: resolved "https://registry.yarnpkg.com/tas-client/-/tas-client-0.2.33.tgz#451bf114a8a64748030ce4068ab7d079958402e6" integrity sha512-V+uqV66BOQnWxvI6HjDnE4VkInmYZUQ4dgB7gzaDyFyFSK1i1nF/j7DpS9UbQAgV9NaF1XpcyuavnM1qOeiEIg== +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + vscode-tas-client@^0.1.84: version "0.1.84" resolved "https://registry.yarnpkg.com/vscode-tas-client/-/vscode-tas-client-0.1.84.tgz#906bdcfd8c9e1dc04321d6bc0335184f9119968e" diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index 25783530305..0b570051b19 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -243,7 +243,7 @@ }, "devDependencies": { "@types/mocha": "^9.1.1", - "@types/node": "18.x" + "@types/node": "20.x" }, "repository": { "type": "git", diff --git a/extensions/vscode-api-tests/src/memfs.ts b/extensions/vscode-api-tests/src/memfs.ts index b7392ae7195..cd422682499 100644 --- a/extensions/vscode-api-tests/src/memfs.ts +++ b/extensions/vscode-api-tests/src/memfs.ts @@ -218,7 +218,7 @@ export class TestFS implements vscode.FileSystemProvider { private _emitter = new vscode.EventEmitter(); private _bufferedEvents: vscode.FileChangeEvent[] = []; - private _fireSoonHandle?: NodeJS.Timer; + private _fireSoonHandle?: NodeJS.Timeout; readonly onDidChangeFile: vscode.Event = this._emitter.event; diff --git a/extensions/vscode-api-tests/yarn.lock b/extensions/vscode-api-tests/yarn.lock index 3c35048cdc7..484fa0c5ac5 100644 --- a/extensions/vscode-api-tests/yarn.lock +++ b/extensions/vscode-api-tests/yarn.lock @@ -7,7 +7,14 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.1.1.tgz#e7c4f1001eefa4b8afbd1eee27a237fee3bf29c4" integrity sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw== -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== diff --git a/extensions/vscode-colorize-tests/package.json b/extensions/vscode-colorize-tests/package.json index eb72136ccf4..159bd29573b 100644 --- a/extensions/vscode-colorize-tests/package.json +++ b/extensions/vscode-colorize-tests/package.json @@ -20,7 +20,7 @@ "jsonc-parser": "^3.2.0" }, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "contributes": { "semanticTokenTypes": [ diff --git a/extensions/vscode-colorize-tests/yarn.lock b/extensions/vscode-colorize-tests/yarn.lock index a7a6fa446ca..88c52293616 100644 --- a/extensions/vscode-colorize-tests/yarn.lock +++ b/extensions/vscode-colorize-tests/yarn.lock @@ -2,12 +2,19 @@ # yarn lockfile v1 -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" jsonc-parser@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz#31ff3f4c2b9793f89c67212627c51c6394f88e76" integrity sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w== + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== diff --git a/extensions/vscode-test-resolver/package.json b/extensions/vscode-test-resolver/package.json index e538d43f21b..8ab2171ddaa 100644 --- a/extensions/vscode-test-resolver/package.json +++ b/extensions/vscode-test-resolver/package.json @@ -33,7 +33,7 @@ "main": "./out/extension", "browser": "./dist/browser/testResolverMain", "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "capabilities": { "untrustedWorkspaces": { diff --git a/extensions/vscode-test-resolver/yarn.lock b/extensions/vscode-test-resolver/yarn.lock index 8a3d10f2b65..1f4b6c2e8b4 100644 --- a/extensions/vscode-test-resolver/yarn.lock +++ b/extensions/vscode-test-resolver/yarn.lock @@ -2,7 +2,14 @@ # yarn lockfile v1 -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== diff --git a/package.json b/package.json index 05d17c70176..181068c7717 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.90.0", - "distro": "fec0321a3182f40f776709b5ac183c59a120a2b6", + "distro": "56ffc94bfc5a29ff6b762de5f80f4250990f2f4d", "author": { "name": "Microsoft Corporation" }, @@ -117,7 +117,7 @@ "@types/kerberos": "^1.1.2", "@types/minimist": "^1.2.1", "@types/mocha": "^9.1.1", - "@types/node": "18.x", + "@types/node": "20.x", "@types/sinon": "^10.0.2", "@types/sinon-test": "^2.4.2", "@types/trusted-types": "^1.0.6", @@ -148,7 +148,7 @@ "cssnano": "^6.0.3", "debounce": "^1.0.0", "deemon": "^1.8.0", - "electron": "28.2.8", + "electron": "29.3.1", "eslint": "8.36.0", "eslint-plugin-header": "3.1.1", "eslint-plugin-jsdoc": "^46.5.0", diff --git a/remote/.yarnrc b/remote/.yarnrc index 4e7208cdf69..60d35d09192 100644 --- a/remote/.yarnrc +++ b/remote/.yarnrc @@ -1,5 +1,5 @@ disturl "https://nodejs.org/dist" -target "18.18.2" -ms_build_id "256117" +target "20.9.0" +ms_build_id "267516" runtime "node" build_from_source "true" diff --git a/src/vs/base/node/shell.ts b/src/vs/base/node/shell.ts index 9da701ffec3..55d97ca3580 100644 --- a/src/vs/base/node/shell.ts +++ b/src/vs/base/node/shell.ts @@ -33,7 +33,7 @@ function getSystemShellUnixLike(os: platform.OperatingSystem, env: platform.IPro } if (!_TERMINAL_DEFAULT_SHELL_UNIX_LIKE) { - let unixLikeTerminal: string | undefined; + let unixLikeTerminal: string | undefined | null; if (platform.isWindows) { unixLikeTerminal = '/bin/bash'; // for WSL } else { diff --git a/src/vs/platform/terminal/node/terminalProcess.ts b/src/vs/platform/terminal/node/terminalProcess.ts index 79be9013c81..a799870b548 100644 --- a/src/vs/platform/terminal/node/terminalProcess.ts +++ b/src/vs/platform/terminal/node/terminalProcess.ts @@ -102,7 +102,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess private _processStartupComplete: Promise | undefined; private _windowsShellHelper: WindowsShellHelper | undefined; private _childProcessMonitor: ChildProcessMonitor | undefined; - private _titleInterval: NodeJS.Timer | null = null; + private _titleInterval: NodeJS.Timeout | null = null; private _writeQueue: IWriteObject[] = []; private _writeTimeout: NodeJS.Timeout | undefined; private _delayedResizer: DelayedResizer | undefined; diff --git a/src/vs/server/node/remoteExtensionHostAgentServer.ts b/src/vs/server/node/remoteExtensionHostAgentServer.ts index 84664bbb39a..9eace08d06a 100644 --- a/src/vs/server/node/remoteExtensionHostAgentServer.ts +++ b/src/vs/server/node/remoteExtensionHostAgentServer.ts @@ -67,7 +67,7 @@ class RemoteExtensionHostAgentServer extends Disposable implements IServerAPI { private readonly _serverRootPath: string; - private shutdownTimer: NodeJS.Timer | undefined; + private shutdownTimer: NodeJS.Timeout | undefined; constructor( private readonly _socketServer: SocketServer, diff --git a/test/automation/package.json b/test/automation/package.json index 8522561fcac..b9dbbec4bb8 100644 --- a/test/automation/package.json +++ b/test/automation/package.json @@ -27,7 +27,7 @@ "devDependencies": { "@types/mkdirp": "^1.0.1", "@types/ncp": "2.0.1", - "@types/node": "18.x", + "@types/node": "20.x", "@types/tmp": "0.2.2", "cpx2": "3.0.0", "npm-run-all": "^4.1.5", diff --git a/test/automation/yarn.lock b/test/automation/yarn.lock index debf88d613c..57b641d2b61 100644 --- a/test/automation/yarn.lock +++ b/test/automation/yarn.lock @@ -21,10 +21,12 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.1.tgz#3b5c3a26393c19b400844ac422bd0f631a94d69d" integrity sha512-aK9jxMypeSrhiYofWWBf/T7O+KwaiAHzM4sveCdWPn71lzUSMimRnKzhXDKfKwV1kWoBo2P1aGgaIYGLf9/ljw== -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@types/tmp@0.2.2": version "0.2.2" @@ -661,6 +663,11 @@ tree-kill@1.2.2: resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + universalify@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" diff --git a/test/integration/browser/package.json b/test/integration/browser/package.json index e39606d605b..e87c8669983 100644 --- a/test/integration/browser/package.json +++ b/test/integration/browser/package.json @@ -8,7 +8,7 @@ }, "devDependencies": { "@types/mkdirp": "^1.0.1", - "@types/node": "18.x", + "@types/node": "20.x", "@types/rimraf": "^2.0.4", "@types/tmp": "0.1.0", "rimraf": "^2.6.1", diff --git a/test/integration/browser/yarn.lock b/test/integration/browser/yarn.lock index 6ecc33e0c71..591b829f1fd 100644 --- a/test/integration/browser/yarn.lock +++ b/test/integration/browser/yarn.lock @@ -33,10 +33,12 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-13.7.0.tgz#b417deda18cf8400f278733499ad5547ed1abec4" integrity sha512-GnZbirvmqZUzMgkFn70c74OQpTTUcCzlhQliTzYjQMqg+hVKcDnxdL19Ne3UdYzdMA/+W3eb646FWn/ZaT1NfQ== -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@types/rimraf@^2.0.4": version "2.0.4" @@ -142,6 +144,11 @@ tree-kill@1.2.2: resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + vscode-uri@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.3.tgz#a95c1ce2e6f41b7549f86279d19f47951e4f4d84" diff --git a/test/smoke/package.json b/test/smoke/package.json index 13728583887..f2574aba8e6 100644 --- a/test/smoke/package.json +++ b/test/smoke/package.json @@ -20,7 +20,7 @@ "@types/mkdirp": "^1.0.1", "@types/mocha": "^9.1.1", "@types/ncp": "2.0.1", - "@types/node": "18.x", + "@types/node": "20.x", "@types/node-fetch": "^2.5.10", "@types/rimraf": "3.0.2", "npm-run-all": "^4.1.5", diff --git a/test/smoke/yarn.lock b/test/smoke/yarn.lock index 00e5dcd85ab..b2c705923a4 100644 --- a/test/smoke/yarn.lock +++ b/test/smoke/yarn.lock @@ -53,10 +53,12 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-13.11.0.tgz#390ea202539c61c8fa6ba4428b57e05bc36dc47b" integrity sha512-uM4mnmsIIPK/yeO+42F2RQhGUIs39K2RFmugcJANppXe6J1nvH87PvzPZYpza7Xhhs8Yn9yIAVdLZ84z61+0xQ== -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@types/rimraf@3.0.2": version "3.0.2" @@ -609,6 +611,11 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + validate-npm-package-license@^3.0.1: version "3.0.4" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" diff --git a/yarn.lock b/yarn.lock index eced3b8d5a3..3720c2fd112 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1253,21 +1253,18 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-4.2.22.tgz#cf488a0f6b4a9c245d09927f4f757ca278b9c8ce" integrity sha512-LXRap3bb4AjtLZ5NOFc4ssVZrQPTgdPcNm++0SEJuJZaOA+xHkojJNYqy33A5q/94BmG5tA6yaMeD4VdCv5aSA== -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x", "@types/node@^20.9.0": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@types/node@^10.11.7": version "10.12.21" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.21.tgz#7e8a0c34cf29f4e17a36e9bd0ea72d45ba03908e" integrity sha512-CBgLNk4o3XMnqMc0rhb6lc77IwShMEglz05deDcn2lQxyXEZivfwgYJu7SMha9V5XcrP6qZuevTHV/QrN2vjKQ== -"@types/node@^18.11.18": - version "18.16.19" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.16.19.tgz#cb03fca8910fdeb7595b755126a8a78144714eea" - integrity sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA== - "@types/responselike@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29" @@ -3848,13 +3845,13 @@ electron-to-chromium@^1.4.668: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.717.tgz#99db370cae8cd090d5b01f8748e9ad369924d0f8" integrity sha512-6Fmg8QkkumNOwuZ/5mIbMU9WI3H2fmn5ajcVya64I5Yr5CcNmO7vcLt0Y7c96DCiMO5/9G+4sI2r6eEvdg1F7A== -electron@28.2.8: - version "28.2.8" - resolved "https://registry.yarnpkg.com/electron/-/electron-28.2.8.tgz#b83d70ca00c0e767f0125fcec85f39aafe39ee4c" - integrity sha512-VgXw2OHqPJkobIC7X9eWh3atptjnELaP+zlbF9Oz00ridlaOWmtLPsp6OaXbLw35URpMr0iYesq8okKp7S0k+g== +electron@29.3.1: + version "29.3.1" + resolved "https://registry.yarnpkg.com/electron/-/electron-29.3.1.tgz#87c82b2cd2c326f78f036499377a5448bea5d4bb" + integrity sha512-auge1/6RVqgUd6TgIq88wKdUCJi2cjESi3jy7d+6X4JzvBGprKBqMJ8JSSFpu/Px1YJrFUKAxfy6SC+TQf1uLw== dependencies: "@electron/get" "^2.0.0" - "@types/node" "^18.11.18" + "@types/node" "^20.9.0" extract-zip "^2.0.1" emoji-regex@^7.0.1: @@ -10084,6 +10081,11 @@ undertaker@^1.2.1: object.reduce "^1.0.0" undertaker-registry "^1.0.0" +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + union-value@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" From 47234ec78828f5db6ef8899065cb259b2a4f98ec Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Fri, 10 May 2024 09:56:15 -0700 Subject: [PATCH 094/357] Clean up chat history variables (#212451) --- .../contrib/chat/browser/chat.contribution.ts | 1 - .../browser/contrib/chatHistoryVariables.ts | 34 ------------------- 2 files changed, 35 deletions(-) delete mode 100644 src/vs/workbench/contrib/chat/browser/contrib/chatHistoryVariables.ts diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 256261d2150..87f9576aa2e 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -40,7 +40,6 @@ import { ChatResponseAccessibleView } from 'vs/workbench/contrib/chat/browser/ch import { ChatVariablesService } from 'vs/workbench/contrib/chat/browser/chatVariables'; import { ChatWidgetService } from 'vs/workbench/contrib/chat/browser/chatWidget'; import { ChatCodeBlockContextProviderService } from 'vs/workbench/contrib/chat/browser/codeBlockContextProviderService'; -import 'vs/workbench/contrib/chat/browser/contrib/chatHistoryVariables'; import 'vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib'; import 'vs/workbench/contrib/chat/browser/contrib/chatInputCompletions'; import { ChatAgentLocation, ChatAgentNameService, ChatAgentService, IChatAgentNameService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatHistoryVariables.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatHistoryVariables.ts deleted file mode 100644 index 1d8dfd377d8..00000000000 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatHistoryVariables.ts +++ /dev/null @@ -1,34 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Disposable } from 'vs/base/common/lifecycle'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; -import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; -import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; - -class ChatHistoryVariables extends Disposable { - constructor( - @IChatVariablesService chatVariablesService: IChatVariablesService, - ) { - super(); - - this._register(chatVariablesService.registerVariable({ name: 'response', description: '', canTakeArgument: true, hidden: true }, async (message, arg, model, progress, token) => { - if (!arg) { - return undefined; - } - - const responseNum = parseInt(arg, 10); - const response = model.getRequests()[responseNum - 1].response; - if (!response) { - return undefined; - } - - return response.response.asString(); - })); - } -} - -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(ChatHistoryVariables, LifecyclePhase.Eventually); From 26c4a07b47a2bd34480a5e4cba2c2384c590b039 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Fri, 10 May 2024 19:19:13 +0200 Subject: [PATCH 095/357] update distro (#212452) --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 181068c7717..51196b2b6a7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.90.0", - "distro": "56ffc94bfc5a29ff6b762de5f80f4250990f2f4d", + "distro": "fdf8ae1d8f85249891bd9670ac010fae9162eecc", "author": { "name": "Microsoft Corporation" }, @@ -226,4 +226,4 @@ "optionalDependencies": { "windows-foreground-love": "0.5.0" } -} +} \ No newline at end of file From ef54c4eb7bfd04bb0485cf9bc407b187b1be1799 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Fri, 10 May 2024 10:24:14 -0700 Subject: [PATCH 096/357] Get rid of bad Copilot Chat warning, add a proper API version (#212453) --- .../workbench/api/common/extHost.api.impl.ts | 7 ++++--- .../contrib/chat/common/chatServiceImpl.ts | 18 +----------------- ...code.proposed.chatParticipantAdditions.d.ts | 8 ++++++++ .../vscode.proposed.interactive.d.ts | 3 --- 4 files changed, 13 insertions(+), 23 deletions(-) diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 186d496596b..66fe037cc20 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1377,9 +1377,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I // namespace: interactive const interactive: typeof vscode.interactive = { - // IMPORTANT - // this needs to be updated whenever the API proposal changes - _version: 1, transferActiveChat(toWorkspace: vscode.Uri) { checkProposedApiEnabled(extension, 'interactive'); return extHostChatAgents2.transferActiveChat(toWorkspace); @@ -1404,6 +1401,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I // namespace: chat const chat: typeof vscode.chat = { + // IMPORTANT + // this needs to be updated whenever the API proposal changes and breaks backwards compatibility + _version: 1, + registerChatResponseProvider(id: string, provider: vscode.ChatResponseProvider, metadata: vscode.ChatResponseProviderMetadata) { checkProposedApiEnabled(extension, 'chatProvider'); return extHostLanguageModels.registerLanguageModel(extension, id, provider, metadata); diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index a43b8b2f1c4..034ad834957 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Action } from 'vs/base/common/actions'; import { coalesce } from 'vs/base/common/arrays'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { ErrorNoTelemetry } from 'vs/base/common/errors'; @@ -15,10 +14,9 @@ import { revive } from 'vs/base/common/marshalling'; import { StopWatch } from 'vs/base/common/stopwatch'; import { URI, UriComponents } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; -import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; -import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { Progress } from 'vs/platform/progress/common/progress'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -154,8 +152,6 @@ export class ChatService extends Disposable implements IChatService { @IChatSlashCommandService private readonly chatSlashCommandService: IChatSlashCommandService, @IChatVariablesService private readonly chatVariablesService: IChatVariablesService, @IChatAgentService private readonly chatAgentService: IChatAgentService, - @INotificationService private readonly notificationService: INotificationService, - @ICommandService private readonly commandService: ICommandService, ) { super(); @@ -358,18 +354,6 @@ export class ChatService extends Disposable implements IChatService { const defaultAgent = this.chatAgentService.getActivatedAgents().find(agent => agent.id === defaultAgentData.id); if (!defaultAgent) { - // Should have been registered during activation above! - this.notificationService.notify({ - severity: Severity.Error, - message: localize('chatFailErrorMessage', "Chat failed to load. Please ensure that the GitHub Copilot Chat extension is up to date."), - actions: { - primary: [ - new Action('showExtension', localize('action.showExtension', "Show Extension"), undefined, true, () => { - return this.commandService.executeCommand('workbench.extensions.action.showExtensionsWithIds', ['GitHub.copilot-chat']); - }) - ] - } - }); throw new ErrorNoTelemetry('No default agent registered'); } const welcomeMessage = model.welcomeMessage ? undefined : await defaultAgent.provideWelcomeMessage?.(model.initialLocation, token) ?? undefined; diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index 260c84c94a2..875849bc8ed 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -215,6 +215,14 @@ declare module 'vscode' { export function createChatParticipant(id: string, handler: ChatExtendedRequestHandler): ChatParticipant; export function createDynamicChatParticipant(id: string, dynamicProps: DynamicChatParticipantProps, handler: ChatExtendedRequestHandler): ChatParticipant; + + /** + * Current version of the proposal. Changes whenever backwards-incompatible changes are made. + * If a new feature is added that doesn't break existing code, the version is not incremented. When the extension uses this new feature, it should set its engines.vscode version appropriately. + * But if a change is made to an existing feature that would break existing code, the version should be incremented. + * The chat extension should not activate if it doesn't support the current version. + */ + export const _version: 1 | number; } /** diff --git a/src/vscode-dts/vscode.proposed.interactive.d.ts b/src/vscode-dts/vscode.proposed.interactive.d.ts index c25195e5aed..d6c4c7b5296 100644 --- a/src/vscode-dts/vscode.proposed.interactive.d.ts +++ b/src/vscode-dts/vscode.proposed.interactive.d.ts @@ -6,9 +6,6 @@ declare module 'vscode' { export namespace interactive { - // current version of the proposal. - export const _version: 1 | number; - export function transferActiveChat(toWorkspace: Uri): void; } } From 58b09734aa5e6cadd22abbf9eb92d26bc6d52d41 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Fri, 10 May 2024 10:55:13 -0700 Subject: [PATCH 097/357] Put terminal copilot hint behind a setting (#212456) --- .../chat/browser/terminal.initialHint.contribution.ts | 6 +++++- .../chat/common/terminalInitialHintConfiguration.ts | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts index 60e40ed0dd2..41f58f9179d 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts @@ -82,6 +82,7 @@ export class TerminalInitialHintContribution extends Disposable implements ITerm widgetManager: TerminalWidgetManager | undefined, @IInlineChatService private readonly _inlineChatService: IInlineChatService, @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IConfigurationService private readonly _configurationService: IConfigurationService, @ITerminalService private readonly _terminalService: ITerminalService ) { super(); @@ -104,6 +105,10 @@ export class TerminalInitialHintContribution extends Disposable implements ITerm return; } + if (!this._configurationService.getValue(TerminalInitialHintSettingId.Enabled)) { + return; + } + if (!this._decoration) { const marker = this._xterm.raw.registerMarker(); if (!marker) { @@ -291,4 +296,3 @@ class TerminalInitialHintWidget extends Disposable { super.dispose(); } } - diff --git a/src/vs/workbench/contrib/terminalContrib/chat/common/terminalInitialHintConfiguration.ts b/src/vs/workbench/contrib/terminalContrib/chat/common/terminalInitialHintConfiguration.ts index 41567ee5921..618f80e1967 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/common/terminalInitialHintConfiguration.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/common/terminalInitialHintConfiguration.ts @@ -16,6 +16,6 @@ export const terminalInitialHintConfiguration: IStringDictionary Date: Fri, 10 May 2024 10:56:29 -0700 Subject: [PATCH 098/357] Fix chat participant name regex (#212458) --- .../contrib/chat/browser/chatParticipantContributions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts b/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts index f5132a7ab55..551fcbea8e7 100644 --- a/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts @@ -41,7 +41,7 @@ const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.regi name: { description: localize('chatParticipantName', "User-facing name for this chat participant. The user will use '@' with this name to invoke the participant."), type: 'string', - pattern: '^[\w0-9_-]+$' + pattern: '^[\\w0-9_-]+$' }, fullName: { markdownDescription: localize('chatParticipantFullName', "The full name of this chat participant, which is shown as the label for responses coming from this participant. If not provided, {0} is used.", '`name`'), From e0cc334d694e906bb567339c876bcbccd32103c9 Mon Sep 17 00:00:00 2001 From: Aaron Munger Date: Fri, 10 May 2024 11:00:45 -0700 Subject: [PATCH 099/357] use real telemetry service (#212459) --- .../notebook/common/notebookEditorModel.ts | 9 ++++--- .../notebookEditorModelResolverServiceImpl.ts | 4 ++- .../test/browser/notebookEditorModel.test.ts | 27 +++++++++++++------ 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index f2e90bd136d..5974292f2e5 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -196,7 +196,8 @@ export class NotebookFileWorkingCopyModel extends Disposable implements IStoredF constructor( private readonly _notebookModel: NotebookTextModel, private readonly _notebookService: INotebookService, - private readonly _configurationService: IConfigurationService + private readonly _configurationService: IConfigurationService, + private readonly _telemetryService: ITelemetryService ) { super(); @@ -257,8 +258,7 @@ export class NotebookFileWorkingCopyModel extends Disposable implements IStoredF isRemote: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Whether the save is happening on a remote file system' }; versionMismatch: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'If the error was because of a version mismatch' }; }; - const telemetry = {} as ITelemetryService; - telemetry.publicLogError2('notebook/SaveError', { + this._telemetryService.publicLogError2('notebook/SaveError', { isRemote: this._notebookModel.uri.scheme === Schemas.vscodeRemote, versionMismatch: error instanceof Error && error.message === 'Document version mismatch' }); @@ -358,6 +358,7 @@ export class NotebookFileWorkingCopyModelFactory implements IStoredFileWorkingCo private readonly _viewType: string, @INotebookService private readonly _notebookService: INotebookService, @IConfigurationService private readonly _configurationService: IConfigurationService, + @ITelemetryService private readonly _telemetryService: ITelemetryService ) { } async createModel(resource: URI, stream: VSBufferReadableStream, token: CancellationToken): Promise { @@ -375,7 +376,7 @@ export class NotebookFileWorkingCopyModelFactory implements IStoredFileWorkingCo } const notebookModel = this._notebookService.createNotebookTextModel(info.viewType, resource, data, info.serializer.options); - return new NotebookFileWorkingCopyModel(notebookModel, this._notebookService, this._configurationService); + return new NotebookFileWorkingCopyModel(notebookModel, this._notebookService, this._configurationService, this._telemetryService); } } diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts index 0a8a17a170d..9d0a90e4576 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts @@ -22,6 +22,7 @@ import { assertIsDefined } from 'vs/base/common/types'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IFileReadLimits } from 'vs/platform/files/common/files'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; class NotebookModelReferenceCollection extends ReferenceCollection> { @@ -43,6 +44,7 @@ class NotebookModelReferenceCollection extends ReferenceCollection>this._instantiationService.createInstance( FileWorkingCopyManager, workingCopyTypeId, diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts index 8d1d368dbd7..071017d82d6 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts @@ -15,6 +15,7 @@ import { TestConfigurationService } from 'vs/platform/configuration/test/common/ import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IFileStatWithMetadata } from 'vs/platform/files/common/files'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { CellKind, IOutputDto, NotebookData, NotebookSetting, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookFileWorkingCopyModel } from 'vs/workbench/contrib/notebook/common/notebookEditorModel'; @@ -27,6 +28,7 @@ suite('NotebookFileWorkingCopyModel', function () { let disposables: DisposableStore; let instantiationService: TestInstantiationService; const configurationService = new TestConfigurationService(); + const telemetryService = new class extends mock() { }; teardown(() => disposables.dispose()); @@ -62,7 +64,8 @@ suite('NotebookFileWorkingCopyModel', function () { } } ), - configurationService + configurationService, + telemetryService )); await model.snapshot(SnapshotContext.Save, CancellationToken.None); @@ -84,7 +87,8 @@ suite('NotebookFileWorkingCopyModel', function () { } } ), - configurationService + configurationService, + telemetryService )); await model.snapshot(SnapshotContext.Save, CancellationToken.None); assert.strictEqual(callCount, 1); @@ -118,7 +122,8 @@ suite('NotebookFileWorkingCopyModel', function () { } } ), - configurationService + configurationService, + telemetryService )); await model.snapshot(SnapshotContext.Save, CancellationToken.None); @@ -140,7 +145,9 @@ suite('NotebookFileWorkingCopyModel', function () { } } ), - configurationService + configurationService, + telemetryService, + )); await model.snapshot(SnapshotContext.Save, CancellationToken.None); assert.strictEqual(callCount, 1); @@ -173,7 +180,8 @@ suite('NotebookFileWorkingCopyModel', function () { } } ), - configurationService + configurationService, + telemetryService )); await model.snapshot(SnapshotContext.Save, CancellationToken.None); @@ -195,7 +203,8 @@ suite('NotebookFileWorkingCopyModel', function () { } } ), - configurationService + configurationService, + telemetryService )); await model.snapshot(SnapshotContext.Save, CancellationToken.None); assert.strictEqual(callCount, 1); @@ -229,7 +238,8 @@ suite('NotebookFileWorkingCopyModel', function () { } } ), - configurationService + configurationService, + telemetryService )); try { @@ -271,7 +281,8 @@ suite('NotebookFileWorkingCopyModel', function () { const model = disposables.add(new NotebookFileWorkingCopyModel( notebook, notebookService, - configurationService + configurationService, + telemetryService )); // the save method should not be set if the serializer is not yet resolved From 3aaf39db0878caaf4172e76972a2384c6713164c Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Fri, 10 May 2024 11:11:48 -0700 Subject: [PATCH 100/357] Keep Copilot Chat compatibility for one more build (#212460) --- src/vs/workbench/api/common/extHost.api.impl.ts | 2 ++ src/vscode-dts/vscode.proposed.interactive.d.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 66fe037cc20..8a34a9e8919 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1377,6 +1377,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I // namespace: interactive const interactive: typeof vscode.interactive = { + // TODO Can be deleted after another Insiders + _version: 1, transferActiveChat(toWorkspace: vscode.Uri) { checkProposedApiEnabled(extension, 'interactive'); return extHostChatAgents2.transferActiveChat(toWorkspace); diff --git a/src/vscode-dts/vscode.proposed.interactive.d.ts b/src/vscode-dts/vscode.proposed.interactive.d.ts index d6c4c7b5296..472f013cca3 100644 --- a/src/vscode-dts/vscode.proposed.interactive.d.ts +++ b/src/vscode-dts/vscode.proposed.interactive.d.ts @@ -6,6 +6,8 @@ declare module 'vscode' { export namespace interactive { + // Can be deleted after another insiders + export const _version: number; export function transferActiveChat(toWorkspace: Uri): void; } } From dda8b2e167a3a89e1cfbca1db05848835b5a5b42 Mon Sep 17 00:00:00 2001 From: David Dossett Date: Fri, 10 May 2024 11:47:36 -0700 Subject: [PATCH 101/357] Fix chat progress message contrast --- src/vs/workbench/contrib/chat/browser/media/chat.css | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index befebd0e43f..3b26978c868 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -49,8 +49,8 @@ } .interactive-item-container .header .detail-container { - font-size: 0.9em; - opacity: 0.7; + font-size: 12px; + color: var(--vscode-descriptionForeground); } .interactive-item-container .chat-animated-ellipsis { @@ -635,8 +635,7 @@ .interactive-session .chat-used-context-label { font-size: 12px; - color: var(--vscode-foreground); - opacity: 0.8; + color: var(--vscode-descriptionForeground); user-select: none; } From 00b84dc3a6e1a1b24f666812cead001d7adb1ee1 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Fri, 10 May 2024 12:34:51 -0700 Subject: [PATCH 102/357] Use hover statusbar for chat "view in marketplace" option --- .../contrib/chat/browser/chatAgentHover.ts | 39 +++++++++---------- .../contrib/chat/browser/chatListRenderer.ts | 5 ++- .../chatMarkdownDecorationsRenderer.ts | 13 ++++--- .../chat/browser/media/chatAgentHover.css | 9 ----- 4 files changed, 29 insertions(+), 37 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts b/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts index 0459ac97b77..63477e5ca15 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts @@ -5,7 +5,7 @@ import * as dom from 'vs/base/browser/dom'; import { h } from 'vs/base/browser/dom'; -import { Button } from 'vs/base/browser/ui/button/button'; +import { IUpdatableHoverOptions } from 'vs/base/browser/ui/hover/hover'; import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Codicon } from 'vs/base/common/codicons'; @@ -55,7 +55,6 @@ export class ChatAgentHover extends Disposable { ]), h('.chat-agent-hover-warning@warning'), h('span.chat-agent-hover-description@description'), - h('span.chat-agent-hover-marketplace-button@button'), ]); this.domNode = hoverElement.root; @@ -76,25 +75,6 @@ export class ChatAgentHover extends Disposable { hoverElement.warning.appendChild(renderIcon(Codicon.warning)); hoverElement.warning.appendChild(dom.$('span', undefined, localize('reservedName', "This chat extension is using a reserved name."))); - - const label = localize('marketplaceLabel', "View in Marketplace") + '.'; - const marketplaceButton = this._register(new Button(hoverElement.button, { - title: label, - buttonBackground: undefined, - buttonBorder: undefined, - buttonForeground: undefined, - buttonHoverBackground: undefined, - buttonSecondaryBackground: undefined, - buttonSecondaryForeground: undefined, - buttonSecondaryHoverBackground: undefined, - buttonSeparator: undefined, - })); - marketplaceButton.label = label; - this._register(marketplaceButton.onDidClick(() => { - if (this.currentAgent) { - this.commandService.executeCommand(showExtensionsWithIdsCommandId, [this.currentAgent.extensionId.value]); - } - })); } setAgent(id: string): void { @@ -140,3 +120,20 @@ export class ChatAgentHover extends Disposable { } } } + +export function getChatAgentHoverOptions(getAgent: () => IChatAgentData | undefined, commandService: ICommandService): IUpdatableHoverOptions { + return { + actions: [ + { + commandId: showExtensionsWithIdsCommandId, + label: localize('marketplaceLabel', "View in Marketplace"), + run: () => { + const agent = getAgent(); + if (agent) { + commandService.executeCommand(showExtensionsWithIdsCommandId, [agent.extensionId.value]); + } + }, + } + ] + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index af062a7f95e..0fd0b1512fd 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -58,7 +58,7 @@ import { ColorScheme } from 'vs/platform/theme/common/theme'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; import { ChatTreeItem, GeneratingPhrase, IChatCodeBlockInfo, IChatFileTreeInfo } from 'vs/workbench/contrib/chat/browser/chat'; -import { ChatAgentHover } from 'vs/workbench/contrib/chat/browser/chatAgentHover'; +import { ChatAgentHover, getChatAgentHoverOptions } from 'vs/workbench/contrib/chat/browser/chatAgentHover'; import { ChatConfirmationWidget } from 'vs/workbench/contrib/chat/browser/chatConfirmationWidget'; import { ChatFollowups } from 'vs/workbench/contrib/chat/browser/chatFollowups'; import { ChatMarkdownDecorationsRenderer } from 'vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer'; @@ -78,6 +78,7 @@ import { ITrustedDomainService } from 'vs/workbench/contrib/url/browser/trustedD import { IMarkdownVulnerability, annotateSpecialMarkdownContent } from '../common/annotations'; import { CodeBlockModelCollection } from '../common/codeBlockModelCollection'; import { IChatListItemRendererOptions } from './chat'; +import { showExtensionsWithIdsCommandId } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; const $ = dom.$; @@ -303,7 +304,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer isResponseVM(template.currentElement) ? template.currentElement.agent : undefined, this.commandService))); const template: IChatListItemTemplate = { avatarContainer, username, detail, referencesListContainer, value, rowContainer, elementDisposables, titleToolbar, templateDisposables, contextKeyService, agentHover }; return template; diff --git a/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts index 9248c95f55f..66a0d120d49 100644 --- a/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts @@ -18,12 +18,14 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { ILogService } from 'vs/platform/log/common/log'; import { asCssVariable } from 'vs/platform/theme/common/colorUtils'; import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; -import { ChatAgentHover } from 'vs/workbench/contrib/chat/browser/chatAgentHover'; +import { ChatAgentHover, getChatAgentHoverOptions } from 'vs/workbench/contrib/chat/browser/chatAgentHover'; import { getFullyQualifiedId, IChatAgentCommand, IChatAgentData, IChatAgentNameService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { chatSlashCommandBackground, chatSlashCommandForeground } from 'vs/workbench/contrib/chat/common/chatColors'; import { chatAgentLeader, ChatRequestAgentPart, ChatRequestDynamicVariablePart, ChatRequestTextPart, chatSubcommandLeader, IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { contentRefUrl } from '../common/annotations'; +import { Lazy } from 'vs/base/common/lazy'; +import { ICommandService } from 'vs/platform/commands/common/commands'; /** For rendering slash commands, variables */ const decorationRefUrl = `http://_vscodedecoration_`; @@ -66,6 +68,7 @@ export class ChatMarkdownDecorationsRenderer { @IChatService private readonly chatService: IChatService, @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, @IChatAgentNameService private readonly chatAgentNameService: IChatAgentNameService, + @ICommandService private readonly commandService: ICommandService, ) { } convertParsedRequestToMarkdown(parsedRequest: IParsedChatRequest): string { @@ -174,11 +177,11 @@ export class ChatMarkdownDecorationsRenderer { container = this.renderResourceWidget(name, undefined); } + const hover: Lazy = new Lazy(() => store.add(this.instantiationService.createInstance(ChatAgentHover))); store.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('element'), container, () => { - const hover = store.add(this.instantiationService.createInstance(ChatAgentHover)); - hover.setAgent(args.agentId); - return hover.domNode; - })); + hover.value.setAgent(args.agentId); + return hover.value.domNode; + }, agent && getChatAgentHoverOptions(() => agent, this.commandService))); return container; } diff --git a/src/vs/workbench/contrib/chat/browser/media/chatAgentHover.css b/src/vs/workbench/contrib/chat/browser/media/chatAgentHover.css index a3573b367b4..1599c4ffeac 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatAgentHover.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatAgentHover.css @@ -76,15 +76,6 @@ } .chat-agent-hover-description, -.chat-agent-hover-marketplace-button .monaco-text-button, .chat-agent-hover-warning { font-size: 13px; } - -.chat-agent-hover .chat-agent-hover-marketplace-button .monaco-text-button { - margin-left: 3px; - display: unset; - padding: unset; - border: unset; - line-height: unset; -} From cb1335537928f1cc76581d469c9263cdabeab357 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Fri, 10 May 2024 12:37:20 -0700 Subject: [PATCH 103/357] Cleanup --- src/vs/workbench/contrib/chat/browser/chatAgentHover.ts | 7 +------ src/vs/workbench/contrib/chat/browser/chatListRenderer.ts | 1 - 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts b/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts index 63477e5ca15..6216dc2248d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts @@ -29,12 +29,9 @@ export class ChatAgentHover extends Disposable { private readonly publisherName: HTMLElement; private readonly description: HTMLElement; - private currentAgent: IChatAgentData | undefined; - constructor( @IChatAgentService private readonly chatAgentService: IChatAgentService, @IExtensionsWorkbenchService private readonly extensionService: IExtensionsWorkbenchService, - @ICommandService private readonly commandService: ICommandService, @IChatAgentNameService private readonly chatAgentNameService: IChatAgentNameService, ) { super(); @@ -79,8 +76,6 @@ export class ChatAgentHover extends Disposable { setAgent(id: string): void { const agent = this.chatAgentService.getAgent(id)!; - this.currentAgent = agent; - if (agent.metadata.icon instanceof URI) { const avatarIcon = dom.$('img.icon'); avatarIcon.src = FileAccess.uriToBrowserUri(agent.metadata.icon).toString(true); @@ -135,5 +130,5 @@ export function getChatAgentHoverOptions(getAgent: () => IChatAgentData | undefi }, } ] - } + }; } diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 0fd0b1512fd..a81b3b1df73 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -78,7 +78,6 @@ import { ITrustedDomainService } from 'vs/workbench/contrib/url/browser/trustedD import { IMarkdownVulnerability, annotateSpecialMarkdownContent } from '../common/annotations'; import { CodeBlockModelCollection } from '../common/codeBlockModelCollection'; import { IChatListItemRendererOptions } from './chat'; -import { showExtensionsWithIdsCommandId } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; const $ = dom.$; From 33de1282ed349de8b327bffac517b6a8c85ca4a8 Mon Sep 17 00:00:00 2001 From: David Dossett Date: Fri, 10 May 2024 13:16:13 -0700 Subject: [PATCH 104/357] Remove sparkle from follow ups --- src/vs/workbench/contrib/chat/browser/chatFollowups.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatFollowups.ts b/src/vs/workbench/contrib/chat/browser/chatFollowups.ts index 29a5ba75b7e..718c863904a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatFollowups.ts +++ b/src/vs/workbench/contrib/chat/browser/chatFollowups.ts @@ -70,13 +70,7 @@ export class ChatFollowups extend button.element.classList.add('interactive-followup-command'); } button.element.ariaLabel = localize('followUpAriaLabel', "Follow up question: {0}", followup.title); - let label = ''; - if (followup.kind === 'reply') { - label = '$(sparkle) ' + baseTitle; - } else { - label = baseTitle; - } - button.label = new MarkdownString(label, { supportThemeIcons: true }); + button.label = new MarkdownString(baseTitle); this._register(button.onDidClick(() => this.clickHandler(followup))); } From 03f2af33a135de28903e87734abd8c5da9512051 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 10 May 2024 13:45:50 -0700 Subject: [PATCH 105/357] testing: polish test coverage bar - Make it look more like the notebook toolbar - Make it an overlay widget so it sticks at the top of the screen - Add a basic 'stacking' mechanism to the editor for overlay widgets. This is simple and doesn't support resizing, but the test coverage bar never resizes, so it's good for the moment. cc @aiday-mar - Add a setting, which is off by default pending UX discussion, for the toolbar. Like sticky scroll/breadcrumbs it can be toggled in a context menu too. - Clean up some duplicated observable methods that I wanted to use. ![](https://memes.peet.io/img/24-05-257d9e42-f599-4f62-bd91-626ef118e205.png) --- src/vs/base/browser/dom.ts | 2 +- src/vs/editor/browser/editorBrowser.ts | 10 + src/vs/editor/browser/view.ts | 3 +- .../overlayWidgets/overlayWidgets.ts | 46 ++- .../widget/diffEditor/diffEditorWidget.ts | 3 +- .../editor/browser/widget/diffEditor/utils.ts | 22 +- .../stickyScroll/browser/stickyScroll.css | 1 + .../browser/stickyScrollWidget.ts | 5 +- src/vs/monaco.d.ts | 9 + .../browser/accessibilitySignalService.ts | 15 +- .../common/platformObservableUtils.ts | 30 ++ .../mergeEditor/browser/model/diffComputer.ts | 2 +- .../contrib/mergeEditor/browser/utils.ts | 13 +- .../browser/view/editors/codeEditorView.ts | 3 +- .../mergeEditor/browser/view/mergeEditor.ts | 3 +- .../mergeEditor/browser/view/viewModel.ts | 2 +- .../browser/codeCoverageDecorations.ts | 299 ++++++++++++++---- .../contrib/testing/browser/media/testing.css | 50 +-- .../testing/browser/testCoverageView.ts | 2 + .../contrib/testing/common/configuration.ts | 7 + .../contrib/testing/common/constants.ts | 1 + .../testing/common/testCoverageService.ts | 33 +- .../testing/common/testingContextKeys.ts | 1 + .../textMateWorkerTokenizerController.ts | 13 +- 24 files changed, 392 insertions(+), 183 deletions(-) create mode 100644 src/vs/platform/observable/common/platformObservableUtils.ts diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index ff113c9baa9..76abbb844ec 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -188,7 +188,7 @@ function _wrapAsStandardKeyboardEvent(handler: (e: IKeyboardEvent) => void): (e: export const addStandardDisposableListener: IAddStandardDisposableListenerSignature = function addStandardDisposableListener(node: HTMLElement, type: string, handler: (event: any) => void, useCapture?: boolean): IDisposable { let wrapHandler = handler; - if (type === 'click' || type === 'mousedown') { + if (type === 'click' || type === 'mousedown' || type === 'contextmenu') { wrapHandler = _wrapAsStandardMouseEvent(getWindow(node), handler); } else if (type === 'keydown' || type === 'keypress' || type === 'keyup') { wrapHandler = _wrapAsStandardKeyboardEvent(handler); diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index cbaa5f01d3b..2985f959335 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -250,11 +250,21 @@ export interface IOverlayWidgetPosition { * The position preference for the overlay widget. */ preference: OverlayWidgetPositionPreference | IOverlayWidgetPositionCoordinates | null; + + /** + * When set, stacks with other overlay widgets with the same preference, + * in an order determined by the ordinal value. + */ + stackOridinal?: number; } /** * An overlay widgets renders on top of the text. */ export interface IOverlayWidget { + /** + * Event fired when the widget layout changes. + */ + onDidLayout?: Event; /** * Render this overlay widget in a location where it could overflow the editor's view dom node. */ diff --git a/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts index a803234c4b4..5558cf28cd4 100644 --- a/src/vs/editor/browser/view.ts +++ b/src/vs/editor/browser/view.ts @@ -634,8 +634,7 @@ export class View extends ViewEventHandler { } public layoutOverlayWidget(widgetData: IOverlayWidgetData): void { - const newPreference = widgetData.position ? widgetData.position.preference : null; - const shouldRender = this._overlayWidgets.setWidgetPosition(widgetData.widget, newPreference); + const shouldRender = this._overlayWidgets.setWidgetPosition(widgetData.widget, widgetData.position); if (shouldRender) { this._scheduleRender(); } diff --git a/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts b/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts index 0953248e2ab..6187175e028 100644 --- a/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts +++ b/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts @@ -5,7 +5,7 @@ import 'vs/css!./overlayWidgets'; import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; -import { IOverlayWidget, IOverlayWidgetPositionCoordinates, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; +import { IOverlayWidget, IOverlayWidgetPosition, IOverlayWidgetPositionCoordinates, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; import { PartFingerprint, PartFingerprints, ViewPart } from 'vs/editor/browser/view/viewPart'; import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/browser/view/renderingContext'; import { ViewContext } from 'vs/editor/common/viewModel/viewContext'; @@ -17,6 +17,7 @@ import * as dom from 'vs/base/browser/dom'; interface IWidgetData { widget: IOverlayWidget; preference: OverlayWidgetPositionPreference | IOverlayWidgetPositionCoordinates | null; + stack?: number; domNode: FastDomNode; } @@ -109,14 +110,17 @@ export class ViewOverlayWidgets extends ViewPart { this._updateMaxMinWidth(); } - public setWidgetPosition(widget: IOverlayWidget, preference: OverlayWidgetPositionPreference | IOverlayWidgetPositionCoordinates | null): boolean { + public setWidgetPosition(widget: IOverlayWidget, position: IOverlayWidgetPosition | null): boolean { const widgetData = this._widgets[widget.getId()]; - if (widgetData.preference === preference) { + const preference = position ? position.preference : null; + const stack = position?.stackOridinal; + if (widgetData.preference === preference && widgetData.stack === stack) { this._updateMaxMinWidth(); return false; } widgetData.preference = preference; + widgetData.stack = stack; this.setShouldRender(); this._updateMaxMinWidth(); @@ -150,7 +154,7 @@ export class ViewOverlayWidgets extends ViewPart { this._context.viewLayout.setOverlayWidgetsMinWidth(maxMinWidth); } - private _renderWidget(widgetData: IWidgetData): void { + private _renderWidget(widgetData: IWidgetData, stackCoordinates: number[]): void { const domNode = widgetData.domNode; if (widgetData.preference === null) { @@ -158,16 +162,29 @@ export class ViewOverlayWidgets extends ViewPart { return; } - if (widgetData.preference === OverlayWidgetPositionPreference.TOP_RIGHT_CORNER) { - domNode.setTop(0); - domNode.setRight((2 * this._verticalScrollbarWidth) + this._minimapWidth); - } else if (widgetData.preference === OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER) { - const widgetHeight = domNode.domNode.clientHeight; - domNode.setTop((this._editorHeight - widgetHeight - 2 * this._horizontalScrollbarHeight)); - domNode.setRight((2 * this._verticalScrollbarWidth) + this._minimapWidth); + const maxRight = (2 * this._verticalScrollbarWidth) + this._minimapWidth; + if (widgetData.preference === OverlayWidgetPositionPreference.TOP_RIGHT_CORNER || widgetData.preference === OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER) { + if (widgetData.preference === OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER) { + domNode.setTop(0); + } else { + const widgetHeight = domNode.domNode.clientHeight; + domNode.setTop((this._editorHeight - widgetHeight - 2 * this._horizontalScrollbarHeight)); + } + + if (widgetData.stack !== undefined) { + domNode.setTop(stackCoordinates[widgetData.preference]); + stackCoordinates[widgetData.preference] += domNode.domNode.clientWidth; + } else { + domNode.setRight(maxRight); + } } else if (widgetData.preference === OverlayWidgetPositionPreference.TOP_CENTER) { - domNode.setTop(0); domNode.domNode.style.right = '50%'; + if (widgetData.stack !== undefined) { + domNode.setTop(stackCoordinates[OverlayWidgetPositionPreference.TOP_CENTER]); + stackCoordinates[OverlayWidgetPositionPreference.TOP_CENTER] += domNode.domNode.clientHeight; + } else { + domNode.setTop(0); + } } else { const { top, left } = widgetData.preference; const fixedOverflowWidgets = this._context.configuration.options.get(EditorOption.fixedOverflowWidgets); @@ -194,9 +211,12 @@ export class ViewOverlayWidgets extends ViewPart { this._domNode.setWidth(this._editorWidth); const keys = Object.keys(this._widgets); + const stackCoordinates = Array.from({ length: OverlayWidgetPositionPreference.TOP_CENTER + 1 }, () => 0); + keys.sort((a, b) => (this._widgets[a].stack || 0) - (this._widgets[b].stack || 0)); + for (let i = 0, len = keys.length; i < len; i++) { const widgetId = keys[i]; - this._renderWidget(this._widgets[widgetId]); + this._renderWidget(this._widgets[widgetId], stackCoordinates); } } } diff --git a/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts index fbaa5ffcabb..5fdc32e47ee 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts @@ -26,7 +26,8 @@ import { HideUnchangedRegionsFeature } from 'vs/editor/browser/widget/diffEditor import { MovedBlocksLinesFeature } from 'vs/editor/browser/widget/diffEditor/features/movedBlocksLinesFeature'; import { OverviewRulerFeature } from 'vs/editor/browser/widget/diffEditor/features/overviewRulerFeature'; import { RevertButtonsFeature } from 'vs/editor/browser/widget/diffEditor/features/revertButtonsFeature'; -import { CSSStyle, ObservableElementSizeObserver, applyStyle, applyViewZones, bindContextKey, readHotReloadableExport, translatePosition } from 'vs/editor/browser/widget/diffEditor/utils'; +import { CSSStyle, ObservableElementSizeObserver, applyStyle, applyViewZones, readHotReloadableExport, translatePosition } from 'vs/editor/browser/widget/diffEditor/utils'; +import { bindContextKey } from 'vs/platform/observable/common/platformObservableUtils'; import { IDiffEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IDimension } from 'vs/editor/common/core/dimension'; import { Position } from 'vs/editor/common/core/position'; diff --git a/src/vs/editor/browser/widget/diffEditor/utils.ts b/src/vs/editor/browser/widget/diffEditor/utils.ts index 65705cb8097..3b968353291 100644 --- a/src/vs/editor/browser/widget/diffEditor/utils.ts +++ b/src/vs/editor/browser/widget/diffEditor/utils.ts @@ -8,7 +8,7 @@ import { findLast } from 'vs/base/common/arraysFind'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { isHotReloadEnabled, registerHotReloadHandler } from 'vs/base/common/hotReload'; import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { IObservable, IReader, ISettableObservable, autorun, autorunHandleChanges, autorunOpts, autorunWithStore, observableFromEvent, observableSignalFromEvent, observableValue, transaction } from 'vs/base/common/observable'; +import { IObservable, IReader, ISettableObservable, autorun, autorunHandleChanges, autorunOpts, autorunWithStore, observableSignalFromEvent, observableValue, transaction } from 'vs/base/common/observable'; import { ElementSizeObserver } from 'vs/editor/browser/config/elementSizeObserver'; import { ICodeEditor, IOverlayWidget, IViewZone } from 'vs/editor/browser/editorBrowser'; import { Position } from 'vs/editor/common/core/position'; @@ -16,8 +16,6 @@ import { Range } from 'vs/editor/common/core/range'; import { DetailedLineRangeMapping } from 'vs/editor/common/diff/rangeMapping'; import { IModelDeltaDecoration } from 'vs/editor/common/model'; import { TextLength } from 'vs/editor/common/core/textLength'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ContextKeyValue, RawContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; export function joinCombine(arr1: readonly T[], arr2: readonly T[], keySelector: (val: T) => number, combine: (v1: T, v2: T) => T): readonly T[] { if (arr1.length === 0) { @@ -89,17 +87,6 @@ export function prependRemoveOnDispose(parent: HTMLElement, child: HTMLElement) }); } -export function observableConfigValue(key: string, defaultValue: T, configurationService: IConfigurationService): IObservable { - return observableFromEvent( - (handleChange) => configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(key)) { - handleChange(e); - } - }), - () => configurationService.getValue(key) ?? defaultValue, - ); -} - export class ObservableElementSizeObserver extends Disposable { private readonly elementSizeObserver: ElementSizeObserver; @@ -440,13 +427,6 @@ function lengthBetweenPositions(position1: Position, position2: Position): TextL } } -export function bindContextKey(key: RawContextKey, service: IContextKeyService, computeValue: (reader: IReader) => T): IDisposable { - const boundKey = key.bindTo(service); - return autorunOpts({ debugName: () => `Set Context Key "${key.key}"` }, reader => { - boundKey.set(computeValue(reader)); - }); -} - export function filterWithPrevious(arr: T[], filter: (cur: T, prev: T | undefined) => boolean): T[] { let prev: T | undefined; return arr.filter(cur => { diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css b/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css index 8afc9c241cf..3bc52c6c915 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css @@ -64,6 +64,7 @@ box-shadow: var(--vscode-editorStickyScroll-shadow) 0 3px 2px -2px; z-index: 4; background-color: var(--vscode-editorStickyScroll-background); + right: initial !important; } .monaco-editor .sticky-widget.peek { diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts index bdcaafb4891..d0e8da4b17a 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts @@ -9,7 +9,7 @@ import { equals } from 'vs/base/common/arrays'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ThemeIcon } from 'vs/base/common/themables'; import 'vs/css!./stickyScroll'; -import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; import { getColumnOfNodeOffset } from 'vs/editor/browser/viewParts/lines/viewLine'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; import { EditorLayoutInfo, EditorOption, RenderLineNumbersType } from 'vs/editor/common/config/editorOptions'; @@ -387,7 +387,8 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { getPosition(): IOverlayWidgetPosition | null { return { - preference: null + preference: OverlayWidgetPositionPreference.TOP_CENTER, + stackOridinal: 10, }; } diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index e2c5bd2ea0b..b9c7cbd73ab 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -5387,12 +5387,21 @@ declare namespace monaco.editor { * The position preference for the overlay widget. */ preference: OverlayWidgetPositionPreference | IOverlayWidgetPositionCoordinates | null; + /** + * When set, stacks with other overlay widgets with the same preference, + * in an order determined by the ordinal value. + */ + stackOridinal?: number; } /** * An overlay widgets renders on top of the text. */ export interface IOverlayWidget { + /** + * Event fired when the widget layout changes. + */ + onDidLayout?: IEvent; /** * Render this overlay widget in a location where it could overflow the editor's view dom node. */ diff --git a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts index ba277dbc24e..3ef15eb1b3a 100644 --- a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts +++ b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts @@ -8,12 +8,13 @@ import { getStructuralKey } from 'vs/base/common/equals'; import { Event, IValueWithChangeEvent } from 'vs/base/common/event'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { FileAccess } from 'vs/base/common/network'; -import { derived, IObservable, observableFromEvent } from 'vs/base/common/observable'; +import { derived, observableFromEvent } from 'vs/base/common/observable'; import { ValueWithChangeEventFromObservable } from 'vs/base/common/observableInternal/utils'; import { localize } from 'vs/nls'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; export const IAccessibilitySignalService = createDecorator('accessibilitySignalService'); @@ -201,7 +202,7 @@ export class AccessibilitySignalService extends Disposable implements IAccessibi private readonly _signalConfigValue = new CachedFunction((signal: AccessibilitySignal) => observableConfigValue<{ sound: EnabledState; announcement: EnabledState; - }>(signal.settingsKey, this.configurationService)); + }>(signal.settingsKey, { sound: 'off', announcement: 'off' }, this.configurationService)); private readonly _signalEnabledState = new CachedFunction( { getCacheKey: getStructuralKey }, @@ -589,13 +590,3 @@ export class AccessibilitySignal { }); } -export function observableConfigValue(key: string, configurationService: IConfigurationService): IObservable { - return observableFromEvent( - (handleChange) => configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(key)) { - handleChange(e); - } - }), - () => configurationService.getValue(key), - ); -} diff --git a/src/vs/platform/observable/common/platformObservableUtils.ts b/src/vs/platform/observable/common/platformObservableUtils.ts new file mode 100644 index 00000000000..096993beb80 --- /dev/null +++ b/src/vs/platform/observable/common/platformObservableUtils.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable } from 'vs/base/common/lifecycle'; +import { autorunOpts, IObservable, IReader, observableFromEvent } from 'vs/base/common/observable'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ContextKeyValue, RawContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; + +/** Creates an observable update when a configuration key updates. */ +export function observableConfigValue(key: string, defaultValue: T, configurationService: IConfigurationService): IObservable { + return observableFromEvent( + (handleChange) => configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(key)) { + handleChange(e); + } + }), + () => configurationService.getValue(key) ?? defaultValue + ); +} + +/** Update the configuration key with a value derived from observables. */ +export function bindContextKey(key: RawContextKey, service: IContextKeyService, computeValue: (reader: IReader) => T): IDisposable { + const boundKey = key.bindTo(service); + return autorunOpts({ debugName: () => `Set Context Key "${key.key}"` }, reader => { + boundKey.set(computeValue(reader)); + }); +} + diff --git a/src/vs/workbench/contrib/mergeEditor/browser/model/diffComputer.ts b/src/vs/workbench/contrib/mergeEditor/browser/model/diffComputer.ts index 278615a60b2..56760afd5eb 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/model/diffComputer.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/model/diffComputer.ts @@ -11,7 +11,7 @@ import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model/lineRange'; import { DetailedLineRangeMapping, RangeMapping } from 'vs/workbench/contrib/mergeEditor/browser/model/mapping'; -import { observableConfigValue } from 'vs/workbench/contrib/mergeEditor/browser/utils'; +import { observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; import { LineRange as DiffLineRange } from 'vs/editor/common/core/lineRange'; export interface IMergeDiffComputer { diff --git a/src/vs/workbench/contrib/mergeEditor/browser/utils.ts b/src/vs/workbench/contrib/mergeEditor/browser/utils.ts index c085272472c..5ac6522a14b 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/utils.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/utils.ts @@ -6,10 +6,9 @@ import { ArrayQueue, CompareResult } from 'vs/base/common/arrays'; import { onUnexpectedError } from 'vs/base/common/errors'; import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { IObservable, autorunOpts, observableFromEvent } from 'vs/base/common/observable'; +import { IObservable, autorunOpts } from 'vs/base/common/observable'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { IModelDeltaDecoration } from 'vs/editor/common/model'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; export function setStyle( @@ -156,13 +155,3 @@ export class PersistentStore { } } -export function observableConfigValue(key: string, defaultValue: T, configurationService: IConfigurationService): IObservable { - return observableFromEvent( - (handleChange) => configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(key)) { - handleChange(e); - } - }), - () => configurationService.getValue(key) ?? defaultValue, - ); -} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts index 8cd243e3777..29af08fbafe 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts @@ -20,7 +20,8 @@ import { MenuId } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { DEFAULT_EDITOR_MAX_DIMENSIONS, DEFAULT_EDITOR_MIN_DIMENSIONS } from 'vs/workbench/browser/parts/editor/editor'; -import { observableConfigValue, setStyle } from 'vs/workbench/contrib/mergeEditor/browser/utils'; +import { setStyle } from 'vs/workbench/contrib/mergeEditor/browser/utils'; +import { observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; import { MergeEditorViewModel } from 'vs/workbench/contrib/mergeEditor/browser/view/viewModel'; export abstract class CodeEditorView extends Disposable { diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts index 3609b0046fa..bec1dec9481 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts @@ -39,7 +39,8 @@ import { readTransientState, writeTransientState } from 'vs/workbench/contrib/co import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; import { IMergeEditorInputModel } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInputModel'; import { MergeEditorModel } from 'vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel'; -import { deepMerge, observableConfigValue, PersistentStore, thenIfNotDisposed } from 'vs/workbench/contrib/mergeEditor/browser/utils'; +import { deepMerge, PersistentStore, thenIfNotDisposed } from 'vs/workbench/contrib/mergeEditor/browser/utils'; +import { observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; import { BaseCodeEditorView } from 'vs/workbench/contrib/mergeEditor/browser/view/editors/baseCodeEditorView'; import { ScrollSynchronizer } from 'vs/workbench/contrib/mergeEditor/browser/view/scrollSynchronizer'; import { MergeEditorViewModel } from 'vs/workbench/contrib/mergeEditor/browser/view/viewModel'; diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts index ca8a00e6b56..1c0f093dc27 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts @@ -15,7 +15,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model/lineRange'; import { MergeEditorModel } from 'vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel'; import { InputNumber, ModifiedBaseRange, ModifiedBaseRangeState } from 'vs/workbench/contrib/mergeEditor/browser/model/modifiedBaseRange'; -import { observableConfigValue } from 'vs/workbench/contrib/mergeEditor/browser/utils'; +import { observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; import { BaseCodeEditorView } from 'vs/workbench/contrib/mergeEditor/browser/view/editors/baseCodeEditorView'; import { CodeEditorView } from 'vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView'; import { InputCodeEditorView } from 'vs/workbench/contrib/mergeEditor/browser/view/editors/inputCodeEditorView'; diff --git a/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts b/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts index a6ce0ce1d1c..c5b1e859205 100644 --- a/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts +++ b/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts @@ -4,16 +4,20 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; +import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { ActionBar, ActionsOrientation, IActionOptions } from 'vs/base/browser/ui/actionbar/actionbar'; +import { renderIcon, renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; +import { Action } from 'vs/base/common/actions'; import { mapFindFirst } from 'vs/base/common/arraysFind'; import { assert, assertNever } from 'vs/base/common/assert'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Lazy } from 'vs/base/common/lazy'; -import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { autorun, derived, observableFromEvent, observableValue } from 'vs/base/common/observable'; import { ThemeIcon } from 'vs/base/common/themables'; -import { ICodeEditor, MouseTargetType } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, MouseTargetType, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; @@ -21,20 +25,24 @@ import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { IModelDecorationOptions, InjectedTextCursorStops, InjectedTextOptions, ITextModel } from 'vs/editor/common/model'; import { localize, localize2 } from 'vs/nls'; import { Categories } from 'vs/platform/action/common/actionCommonCategories'; -import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ILogService } from 'vs/platform/log/common/log'; +import { observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; import { IQuickInputService, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; import * as coverUtils from 'vs/workbench/contrib/testing/browser/codeCoverageDisplayUtils'; -import { testingCoverageMissingBranch } from 'vs/workbench/contrib/testing/browser/icons'; +import { testingCoverageIcon, testingCoverageMissingBranch, testingFilterIcon, testingRerunIcon } from 'vs/workbench/contrib/testing/browser/icons'; import { ManagedTestCoverageBars } from 'vs/workbench/contrib/testing/browser/testCoverageBars'; import { getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration'; +import { TestCommandId } from 'vs/workbench/contrib/testing/common/constants'; import { FileCoverage } from 'vs/workbench/contrib/testing/common/testCoverage'; import { ITestCoverageService } from 'vs/workbench/contrib/testing/common/testCoverageService'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; +import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; import { CoverageDetails, DetailType, IDeclarationCoverage, IStatementCoverage } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; @@ -52,7 +60,7 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri private loadingCancellation?: CancellationTokenSource; private readonly displayedStore = this._register(new DisposableStore()); private readonly hoveredStore = this._register(new DisposableStore()); - private readonly summaryWidget: Lazy; + private readonly summaryWidget: Lazy; private decorationIds = new Map this._register(instantiationService.createInstance(CoverageSummaryWidget, this.editor))); + this.summaryWidget = new Lazy(() => this._register(instantiationService.createInstance(CoverageToolbarWidget, this.editor))); const modelObs = observableFromEvent(editor.onDidChangeModel, () => editor.getModel()); const configObs = observableFromEvent(editor.onDidChangeConfiguration, i => i); @@ -108,6 +117,16 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri } })); + const toolbarEnabled = observableConfigValue(TestingConfigKeys.CoverageToolbarEnabled, true, configurationService); + this._register(autorun(reader => { + const c = fileCoverage.read(reader); + if (c && toolbarEnabled.read(reader)) { + this.summaryWidget.value.setCoverage(c); + } else { + this.summaryWidget.rawValue?.setCoverage(undefined); + } + })); + this._register(autorun(reader => { const c = fileCoverage.read(reader); if (c) { @@ -245,7 +264,6 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri } this.displayedStore.clear(); - this.summaryWidget.value.setCoverage(coverage); model.changeDecorations(e => { for (const detailRange of details.ranges) { @@ -309,8 +327,6 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri }); this.displayedStore.add(toDisposable(() => { - this.summaryWidget.value.setCoverage(undefined); - model.changeDecorations(e => { for (const decoration of this.decorationIds.keys()) { e.removeDecoration(decoration); @@ -513,42 +529,81 @@ function wrapName(functionNameOrCode: string) { return wrapInBackticks(functionNameOrCode); } -class CoverageSummaryWidget implements IDisposable { +class CoverageToolbarWidget extends Disposable implements IOverlayWidget { private current: FileCoverage | undefined; private registered = false; - private readonly registration = new DisposableStore(); - + private isRunning = false; + private readonly showStore = this._register(new DisposableStore()); + private readonly actionBar: ActionBar; private readonly _domNode = dom.h('div.coverage-summary-widget', [ dom.h('div', [ dom.h('span.bars@bars'), dom.h('span.stat@stat'), - dom.h('a.toggleInline@toggleInline'), - dom.h('a.perTestFilter@perTestFilter'), + dom.h('span.toolbar@toolbar'), ]), ]); private readonly bars: ManagedTestCoverageBars; - constructor( private readonly editor: ICodeEditor, @IConfigurationService private readonly configurationService: IConfigurationService, @IQuickInputService private readonly quickInputService: IQuickInputService, @ITestCoverageService private readonly testCoverageService: ITestCoverageService, - @IKeybindingService keybindingService: IKeybindingService, + @IContextMenuService private readonly contextMenuService: IContextMenuService, + @ITestService private readonly testService: ITestService, + @IKeybindingService private readonly keybindingService: IKeybindingService, @IInstantiationService instaService: IInstantiationService, ) { - this._domNode.perTestFilter.ariaLabel = this._domNode.perTestFilter.title = coverUtils.labels.clickToChangeFiltering; - this.bars = instaService.createInstance(ManagedTestCoverageBars, { + super(); + + this.bars = this._register(instaService.createInstance(ManagedTestCoverageBars, { compact: false, overall: false, container: this._domNode.bars, - }); + })); - const kb = keybindingService.lookupKeybinding(TOGGLE_INLINE_COMMAND_ID); - if (kb) { - this._domNode.toggleInline.title = `${TOGGLE_INLINE_COMMAND_TEXT} (${kb.getLabel()})`; - } + this.actionBar = this._register(instaService.createInstance(ActionBar, this._domNode.toolbar, { + orientation: ActionsOrientation.HORIZONTAL, + actionViewItemProvider: (action, options) => { + const vm = new CodiconActionViewItem(undefined, action, options); + if (action instanceof ActionWithIcon) { + vm.themeIcon = action.icon; + } + return vm; + } + })); + + + this._register(autorun(reader => { + CodeCoverageDecorations.showInline.read(reader); + this.setActions(); + })); + + this._register(dom.addStandardDisposableListener(this._domNode.root, dom.EventType.CONTEXT_MENU, e => { + this.contextMenuService.showContextMenu({ + menuId: MenuId.StickyScrollContext, + getAnchor: () => e, + }); + })); + } + + /** @inheritdoc */ + public getId(): string { + return 'coverage-summary-widget'; + } + + /** @inheritdoc */ + public getDomNode(): HTMLElement { + return this._domNode.root; + } + + /** @inheritdoc */ + public getPosition(): IOverlayWidgetPosition | null { + return { + preference: OverlayWidgetPositionPreference.TOP_CENTER, + stackOridinal: 9, + }; } public setCoverage(coverage: FileCoverage | undefined) { @@ -556,32 +611,13 @@ class CoverageSummaryWidget implements IDisposable { this.bars.setCoverageInfo(coverage); if (!coverage) { - return this.unregister(); + return this.hide(); } const displayStat = coverUtils.calculateDisplayedStat(coverage, getTestingConfiguration(this.configurationService, TestingConfigKeys.CoveragePercent)); this._domNode.stat.innerText = localize('testing.percentCoverage', '{0} Coverage', coverUtils.displayPercent(displayStat)); - - this._domNode.perTestFilter.classList.toggle('active', !!coverage.isForTest); - if (coverage.isForTest) { - const testItem = coverage.fromResult.getTestById(coverage.isForTest.id.toString()); - assert(!!testItem, 'got coverage for an unreported test'); - this._domNode.perTestFilter.style.display = 'inline'; - this._domNode.perTestFilter.innerText = coverUtils.labels.showingFilterFor(testItem.label); - } else if (coverage.perTestData?.size) { - this._domNode.perTestFilter.style.display = 'inline'; - this._domNode.perTestFilter.innerText = localize('testing.coverageForTestAvailable', "{0} test(s) in this file", coverage.perTestData.size); - } else { - this._domNode.perTestFilter.style.display = 'none'; - } - - this.register(); - } - - /** @inheritdoc */ - public dispose() { - this.unregister(); - this.bars.dispose(); + this.setActions(); + this.show(); } private filterTest() { @@ -603,65 +639,144 @@ class CoverageSummaryWidget implements IDisposable { ...tests.map(item => ({ label: coverUtils.getLabelForItem(result, item.isForTest!.id, commonPrefix), description: coverUtils.labels.percentCoverage(item.tpc), item })), ]; + // These handle the behavior that reveals the start of coverage when the + // user picks from the quickpick. Scroll position is restored if the user + // exits without picking an item, or picks "all tets". + const scrollTop = this.editor.getScrollTop(); + const revealScrollCts = new MutableDisposable(); + this.quickInputService.pick(items, { activeItem: items.find((item): item is TItem => 'item' in item && item.item === this.current), placeHolder: coverUtils.labels.pickShowCoverage, onDidFocus: (entry) => { - this.testCoverageService.filterToTest.set(entry.item?.isForTest!.id, undefined); + if (!entry.item) { + revealScrollCts.clear(); + this.editor.setScrollTop(scrollTop); + this.testCoverageService.filterToTest.set(undefined, undefined); + } else { + const cts = revealScrollCts.value = new CancellationTokenSource(); + entry.item.details(cts.token).then( + details => { + const first = details.find(d => d.type === DetailType.Statement); + if (!cts.token.isCancellationRequested && first) { + this.editor.revealLineNearTop(first.location instanceof Position ? first.location.lineNumber : first.location.startLineNumber); + } + }, + () => { /* ignored */ } + ); + this.testCoverageService.filterToTest.set(entry.item.isForTest!.id, undefined); + } }, }).then(selected => { + if (!selected) { + this.editor.setScrollTop(scrollTop); + } + + revealScrollCts.dispose(); this.testCoverageService.filterToTest.set(selected ? selected.item?.isForTest!.id : previousSelection, undefined); }); } - private register() { + private setActions() { + this.actionBar.clear(); + const coverage = this.current; + if (!coverage) { + return; + } + + const toggleAction = new ActionWithIcon( + 'toggleInline', + CodeCoverageDecorations.showInline.get() + ? localize('testing.hideInlineCoverage', 'Hide Inline Coverage') + : localize('testing.showInlineCoverage', 'Show Inline Coverage'), + testingCoverageIcon, + undefined, + () => CodeCoverageDecorations.showInline.set(!CodeCoverageDecorations.showInline.get(), undefined), + ); + + const kb = this.keybindingService.lookupKeybinding(TOGGLE_INLINE_COMMAND_ID); + if (kb) { + toggleAction.tooltip = `${TOGGLE_INLINE_COMMAND_TEXT} (${kb.getLabel()})`; + } + + this.actionBar.push(toggleAction); + + if (coverage.isForTest) { + const testItem = coverage.fromResult.getTestById(coverage.isForTest.id.toString()); + assert(!!testItem, 'got coverage for an unreported test'); + this.actionBar.push(new ActionWithIcon('perTestFilter', + coverUtils.labels.showingFilterFor(testItem.label), + testingFilterIcon, + undefined, + () => this.filterTest(), + )); + } else if (coverage.perTestData?.size) { + this.actionBar.push(new ActionWithIcon('perTestFilter', + localize('testing.coverageForTestAvailable', "{0} test(s) in this file", coverage.perTestData.size), + testingFilterIcon, + undefined, + () => this.filterTest(), + )); + } + + this.actionBar.push(new ActionWithIcon( + 'rerun', + localize('testing.rerun', 'Rerun'), + testingRerunIcon, + !this.isRunning, + () => this.rerunTest() + )); + } + + private show() { if (this.registered) { return; } this.registered = true; - let viewZoneId: string; + const ds = this.showStore; + + this.editor.addOverlayWidget(this); this.editor.changeViewZones(accessor => { - viewZoneId = accessor.addZone({ + viewZoneId = accessor.addZone({ // make space for the widget afterLineNumber: 0, afterColumn: 0, - domNode: this._domNode.root, + domNode: document.createElement('div'), heightInPx: 30, ordinal: -1, // show before code lenses }); }); - this.registration.add(toDisposable(() => { + ds.add(toDisposable(() => { + this.registered = false; + this.editor.removeOverlayWidget(this); this.editor.changeViewZones(accessor => { accessor.removeZone(viewZoneId); }); - this.registered = false; })); - this.registration.add(dom.addStandardDisposableListener(this._domNode.perTestFilter, 'click', () => { - this.filterTest(); - })); - - this.registration.add(this.configurationService.onDidChangeConfiguration(e => { + ds.add(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(TestingConfigKeys.CoverageBarThresholds) || e.affectsConfiguration(TestingConfigKeys.CoveragePercent)) { this.setCoverage(this.current); } })); - - this.registration.add(dom.addStandardDisposableListener(this._domNode.toggleInline, 'click', () => { - CodeCoverageDecorations.showInline.set(!CodeCoverageDecorations.showInline.get(), undefined); - })); - - this.registration.add(autorun(reader => { - this._domNode.toggleInline.innerText = CodeCoverageDecorations.showInline.read(reader) - ? localize('testing.hideInlineCoverage', 'Hide Inline Coverage') - : localize('testing.showInlineCoverage', 'Show Inline Coverage'); - })); } - private unregister() { - this.registration.clear(); + private rerunTest() { + const current = this.current; + if (current) { + this.isRunning = true; + this.setActions(); + this.testService.runResolvedTests(current.fromResult.request).finally(() => { + this.isRunning = false; + this.setActions(); + }); + } + } + + private hide() { + this.showStore.clear(); } } @@ -683,3 +798,47 @@ registerAction2(class ToggleInlineCoverage extends Action2 { CodeCoverageDecorations.showInline.set(!CodeCoverageDecorations.showInline.get(), undefined); } }); + +registerAction2(class ToggleCoverageToolbar extends Action2 { + constructor() { + super({ + id: TestCommandId.CoverageToggleToolbar, + title: localize2('testing.toggleToolbarTitle', "Toggle Coverage Toolbar"), + metadata: { + description: localize2('testing.toggleToolbarDesc', 'Toggle the sticky coverage bar in the editor.') + }, + category: Categories.Test, + toggled: { + condition: TestingContextKeys.coverageToolbarEnabled, + title: localize('cmd.toggle2', "Toggle Coverage Toolbar"), + }, + menu: [ + { id: MenuId.CommandPalette, when: TestingContextKeys.isTestCoverageOpen }, + { id: MenuId.StickyScrollContext, when: TestingContextKeys.isTestCoverageOpen }, + ] + }); + } + + run(accessor: ServicesAccessor): void { + const config = accessor.get(IConfigurationService); + const value = getTestingConfiguration(config, TestingConfigKeys.CoverageToolbarEnabled); + config.updateValue(TestingConfigKeys.CoverageToolbarEnabled, !value); + } +}); + +class ActionWithIcon extends Action { + constructor(id: string, title: string, public readonly icon: ThemeIcon, enabled: boolean | undefined, run: () => void) { + super(id, title, undefined, enabled, run); + } +} + +class CodiconActionViewItem extends ActionViewItem { + + public themeIcon?: ThemeIcon; + + protected override updateLabel(): void { + if (this.options.label && this.label && this.themeIcon) { + dom.reset(this.label, renderIcon(this.themeIcon), this.action.label); + } + } +} diff --git a/src/vs/workbench/contrib/testing/browser/media/testing.css b/src/vs/workbench/contrib/testing/browser/media/testing.css index 4271a033ccc..3068f3db57f 100644 --- a/src/vs/workbench/contrib/testing/browser/media/testing.css +++ b/src/vs/workbench/contrib/testing/browser/media/testing.css @@ -404,50 +404,50 @@ .coverage-summary-widget { color: var(--vscode-editor-foreground); z-index: 1; - line-height: 25px; + background: var(--vscode-editor-background); + left: 0; + width: 100%; + box-shadow: var(--vscode-editorStickyScroll-shadow) 0 3px 2px -2px; > div { display: flex; align-items: center; - border-bottom: 1px solid var(--vscode-menu-border); + padding: 0 22px; + height: 25px; } - .toggleInline, .perTestFilter { - border-left: 1px solid var(--vscode-menu-border); - padding: 0 6px; - } - - .stat, .toggleInline { - padding-right: 6px; - } - - > span, > a { - display: inline; + .btn { position: relative; - padding: 0 6px; + margin: 0 4px; + padding: 0 4px; &:first-child { - padding-left: 0; + margin-left: 0; } &:last-child { - padding-right: 0; + margin-right: 0; } } - - a { - color: var(--vscode-textLink-foreground); - cursor: pointer; + .stat, .action-label { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + margin: 0 3px; } - a:hover { - color: var(--vscode-textLink-activeForeground); + .action-label { + display: flex; + align-items: center; + font-size: 13px; + padding: 0 4px; + + .codicon { + margin-right: 4px; + } } - .toggleInline, .perTestFilter { - border-left: 1px solid var(--vscode-menu-border); - } } .test-coverage-tree-per-test-switcher { diff --git a/src/vs/workbench/contrib/testing/browser/testCoverageView.ts b/src/vs/workbench/contrib/testing/browser/testCoverageView.ts index ff7bda28075..841903f8096 100644 --- a/src/vs/workbench/contrib/testing/browser/testCoverageView.ts +++ b/src/vs/workbench/contrib/testing/browser/testCoverageView.ts @@ -23,6 +23,7 @@ import { URI } from 'vs/base/common/uri'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { localize, localize2 } from 'vs/nls'; +import { Categories } from 'vs/platform/action/common/actionCommonCategories'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -685,6 +686,7 @@ registerAction2(class TestCoverageChangePerTestFilterAction extends Action2 { constructor() { super({ id: TestCommandId.CoverageFilterToTest, + category: Categories.Test, title: localize2('testing.changeCoverageFilter', 'Filter Coverage by Test...'), precondition: TestingContextKeys.hasPerTestCoverage, f1: true, diff --git a/src/vs/workbench/contrib/testing/common/configuration.ts b/src/vs/workbench/contrib/testing/common/configuration.ts index 1bbc290e9cd..ec9e50d67f0 100644 --- a/src/vs/workbench/contrib/testing/common/configuration.ts +++ b/src/vs/workbench/contrib/testing/common/configuration.ts @@ -23,6 +23,7 @@ export const enum TestingConfigKeys { CoveragePercent = 'testing.displayedCoveragePercent', ShowCoverageInExplorer = 'testing.showCoverageInExplorer', CoverageBarThresholds = 'testing.coverageBarThresholds', + CoverageToolbarEnabled = 'testing.coverageToolbarEnabled', } export const enum AutoOpenTesting { @@ -190,6 +191,11 @@ export const testingConfiguration: IConfigurationNode = { green: { type: 'number', minimum: 0, maximum: 100, default: 90 }, }, }, + [TestingConfigKeys.CoverageToolbarEnabled]: { + description: localize('testing.coverageToolbarEnabled', 'Controls whether the coverage toolbar is shown in the editor.'), + type: 'boolean', + default: false, // todo@connor4312: disabled by default until UI sync + }, } }; @@ -214,6 +220,7 @@ export interface ITestingConfiguration { [TestingConfigKeys.CoveragePercent]: TestingDisplayedCoveragePercent; [TestingConfigKeys.ShowCoverageInExplorer]: boolean; [TestingConfigKeys.CoverageBarThresholds]: ITestingCoverageBarThresholds; + [TestingConfigKeys.CoverageToolbarEnabled]: boolean; } export const getTestingConfiguration = (config: IConfigurationService, key: K) => config.getValue(key); diff --git a/src/vs/workbench/contrib/testing/common/constants.ts b/src/vs/workbench/contrib/testing/common/constants.ts index 2dfd8cf55c2..e879003b6a1 100644 --- a/src/vs/workbench/contrib/testing/common/constants.ts +++ b/src/vs/workbench/contrib/testing/common/constants.ts @@ -68,6 +68,7 @@ export const enum TestCommandId { CoverageFilterToTest = 'testing.coverageFilterToTest', CoverageLastRun = 'testing.coverageLastRun', CoverageSelectedAction = 'testing.coverageSelected', + CoverageToggleToolbar = 'testing.coverageToggleToolbar', CoverageViewChangeSorting = 'testing.coverageViewChangeSorting', DebugAction = 'testing.debug', DebugAllAction = 'testing.debugAll', diff --git a/src/vs/workbench/contrib/testing/common/testCoverageService.ts b/src/vs/workbench/contrib/testing/common/testCoverageService.ts index 1336f748f86..99e86e86a95 100644 --- a/src/vs/workbench/contrib/testing/common/testCoverageService.ts +++ b/src/vs/workbench/contrib/testing/common/testCoverageService.ts @@ -6,8 +6,11 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { IObservable, ISettableObservable, observableValue, transaction } from 'vs/base/common/observable'; -import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { bindContextKey, observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; +import { TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration'; import { Testing } from 'vs/workbench/contrib/testing/common/constants'; import { TestCoverage } from 'vs/workbench/contrib/testing/common/testCoverage'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; @@ -45,8 +48,6 @@ export interface ITestCoverageService { export class TestCoverageService extends Disposable implements ITestCoverageService { declare readonly _serviceBrand: undefined; - private readonly _isOpenKey: IContextKey; - private readonly _hasPerTestCoverage: IContextKey; private readonly lastOpenCts = this._register(new MutableDisposable()); public readonly selected = observableValue('testCoverage', undefined); @@ -55,11 +56,29 @@ export class TestCoverageService extends Disposable implements ITestCoverageServ constructor( @IContextKeyService contextKeyService: IContextKeyService, @ITestResultService resultService: ITestResultService, + @IConfigurationService configService: IConfigurationService, @IViewsService private readonly viewsService: IViewsService, ) { super(); - this._isOpenKey = TestingContextKeys.isTestCoverageOpen.bindTo(contextKeyService); - this._hasPerTestCoverage = TestingContextKeys.hasPerTestCoverage.bindTo(contextKeyService); + + const toolbarConfig = observableConfigValue(TestingConfigKeys.CoverageToolbarEnabled, true, configService); + this._register(bindContextKey( + TestingContextKeys.coverageToolbarEnabled, + contextKeyService, + reader => toolbarConfig.read(reader), + )); + + this._register(bindContextKey( + TestingContextKeys.isTestCoverageOpen, + contextKeyService, + reader => !!this.selected.read(reader), + )); + + this._register(bindContextKey( + TestingContextKeys.hasPerTestCoverage, + contextKeyService, + reader => !!this.selected.read(reader)?.perTestCoverageIDs.size, + )); this._register(resultService.onResultsChanged(evt => { if ('completed' in evt) { @@ -92,8 +111,6 @@ export class TestCoverageService extends Disposable implements ITestCoverageServ this.filterToTest.set(undefined, tx); this.selected.set(coverage, tx); }); - this._isOpenKey.set(true); - this._hasPerTestCoverage.set(coverage.perTestCoverageIDs.size > 0); if (focus && !cts.token.isCancellationRequested) { this.viewsService.openView(Testing.CoverageViewId, true); @@ -102,8 +119,6 @@ export class TestCoverageService extends Disposable implements ITestCoverageServ /** @inheritdoc */ public closeCoverage() { - this._isOpenKey.set(false); - this._hasPerTestCoverage.set(false); this.selected.set(undefined, undefined); } } diff --git a/src/vs/workbench/contrib/testing/common/testingContextKeys.ts b/src/vs/workbench/contrib/testing/common/testingContextKeys.ts index 7878be0ec9e..2c3d0b8c79f 100644 --- a/src/vs/workbench/contrib/testing/common/testingContextKeys.ts +++ b/src/vs/workbench/contrib/testing/common/testingContextKeys.ts @@ -23,6 +23,7 @@ export namespace TestingContextKeys { export const activeEditorHasTests = new RawContextKey('testing.activeEditorHasTests', false, { type: 'boolean', description: localize('testing.activeEditorHasTests', 'Indicates whether any tests are present in the current editor') }); export const isTestCoverageOpen = new RawContextKey('testing.isTestCoverageOpen', false, { type: 'boolean', description: localize('testing.isTestCoverageOpen', 'Indicates whether a test coverage report is open') }); export const hasPerTestCoverage = new RawContextKey('testing.hasPerTestCoverage', false, { type: 'boolean', description: localize('testing.hasPerTestCoverage', 'Indicates whether per-test coverage is available') }); + export const coverageToolbarEnabled = new RawContextKey('testing.coverageToolbarEnabled', true, { type: 'boolean', description: localize('testing.coverageToolbarEnabled', 'Indicates whether the coverage toolbar is enabled') }); export const capabilityToContextKey: { [K in TestRunProfileBitset]: RawContextKey } = { [TestRunProfileBitset.Run]: hasRunnableTests, diff --git a/src/vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController.ts b/src/vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController.ts index 850b58e1e6c..3695379f0e9 100644 --- a/src/vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController.ts +++ b/src/vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController.ts @@ -5,7 +5,7 @@ import { importAMDNodeModule } from 'vs/amdX'; import { Disposable } from 'vs/base/common/lifecycle'; -import { IObservable, autorun, keepObserved, observableFromEvent } from 'vs/base/common/observable'; +import { IObservable, autorun, keepObserved } from 'vs/base/common/observable'; import { countEOL } from 'vs/editor/common/core/eolCounter'; import { LineRange } from 'vs/editor/common/core/lineRange'; import { Range } from 'vs/editor/common/core/range'; @@ -15,6 +15,7 @@ import { TokenizationStateStore } from 'vs/editor/common/model/textModelTokens'; import { IModelContentChange, IModelContentChangedEvent } from 'vs/editor/common/textModelEvents'; import { ContiguousMultilineTokensBuilder } from 'vs/editor/common/tokens/contiguousMultilineTokensBuilder'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; import { ArrayEdit, MonotonousIndexTransformer, SingleArrayEdit } from 'vs/workbench/services/textMate/browser/arrayOperation'; import type { StateDeltas, TextMateTokenizationWorker } from 'vs/workbench/services/textMate/browser/backgroundTokenization/worker/textMateTokenizationWorker.worker'; import type { applyStateStackDiff, StateStack } from 'vscode-textmate'; @@ -237,13 +238,3 @@ function changesToString(changes: IModelContentChange[]): string { return changes.map(c => Range.lift(c.range).toString() + ' => ' + c.text).join(' & '); } -function observableConfigValue(key: string, defaultValue: T, configurationService: IConfigurationService): IObservable { - return observableFromEvent( - (handleChange) => configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(key)) { - handleChange(e); - } - }), - () => configurationService.getValue(key) ?? defaultValue, - ); -} From b0370688b8949f5715d42294a38205546c92fdd3 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 10 May 2024 14:10:41 -0700 Subject: [PATCH 106/357] fix compile --- .../contrib/testing/browser/codeCoverageDecorations.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts b/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts index c5b1e859205..9040875421a 100644 --- a/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts +++ b/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts @@ -5,8 +5,8 @@ import * as dom from 'vs/base/browser/dom'; import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; -import { ActionBar, ActionsOrientation, IActionOptions } from 'vs/base/browser/ui/actionbar/actionbar'; -import { renderIcon, renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; +import { ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; +import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { Action } from 'vs/base/common/actions'; import { mapFindFirst } from 'vs/base/common/arraysFind'; import { assert, assertNever } from 'vs/base/common/assert'; From 57dde0a028111368757f18abe31565a42a16a2e0 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Fri, 10 May 2024 15:07:48 -0700 Subject: [PATCH 107/357] Fix some participant names being rendered incorrectly after reload (#212472) * Fix some participant names being rendered incorrectly after reload Compute the name using the real persisted IChatAgentData that we already have, instead of looking it up in the service that doesn't have that agent yet. Ideally it would use both- this will still render the old details of agents after they are updated * Fix missing 'fullName' and don't wait for extension activation to get the welcome message fullName * Fix build --- .../contrib/chat/browser/chat.contribution.ts | 4 +- .../chatMarkdownDecorationsRenderer.ts | 41 +++++++++---------- .../contrib/chat/common/chatAgents.ts | 1 + .../contrib/chat/common/chatModel.ts | 2 +- 4 files changed, 25 insertions(+), 23 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 87f9576aa2e..8e59f6cac56 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -149,6 +149,7 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable { @ICommandService commandService: ICommandService, @IChatAgentService chatAgentService: IChatAgentService, @IChatVariablesService chatVariablesService: IChatVariablesService, + @IInstantiationService instantiationService: IInstantiationService, ) { super(); this._store.add(slashCommandService.registerSlashCommand({ @@ -184,7 +185,8 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable { .filter(a => a.locations.includes(ChatAgentLocation.Panel)) .map(async a => { const description = a.description ? `- ${a.description}` : ''; - const agentLine = `- ${agentToMarkdown(a, true)} ${description}`; + const agentMarkdown = instantiationService.invokeFunction(accessor => agentToMarkdown(a, true, accessor)); + const agentLine = `- ${agentMarkdown} ${description}`; const commandText = a.slashCommands.map(c => { const description = c.description ? `- ${c.description}` : ''; return `\t* ${agentSlashCommandToMarkdown(a, c)} ${description}`; diff --git a/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts index 66a0d120d49..b62beef0cf4 100644 --- a/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts @@ -12,7 +12,7 @@ import { revive } from 'vs/base/common/marshalling'; import { URI } from 'vs/base/common/uri'; import { Location } from 'vs/editor/common/languages'; import { IHoverService } from 'vs/platform/hover/browser/hover'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ILabelService } from 'vs/platform/label/common/label'; import { ILogService } from 'vs/platform/log/common/log'; @@ -36,13 +36,24 @@ const agentRefUrl = `http://_chatagent_`; /** For rendering agent decorations with hover */ const agentSlashRefUrl = `http://_chatslash_`; -export function agentToMarkdown(agent: IChatAgentData, isClickable: boolean): string { - const args: IAgentWidgetArgs = { agentId: agent.id, isClickable }; +export function agentToMarkdown(agent: IChatAgentData, isClickable: boolean, accessor: ServicesAccessor): string { + const chatAgentNameService = accessor.get(IChatAgentNameService); + const chatAgentService = accessor.get(IChatAgentService); + + const isAllowed = chatAgentNameService.getAgentNameRestriction(agent).get(); + let name = `${isAllowed ? agent.name : getFullyQualifiedId(agent)}`; + const isDupe = isAllowed && chatAgentService.getAgentsByName(agent.name).length > 1; + if (isDupe) { + name += ` (${agent.publisherDisplayName})`; + } + + const args: IAgentWidgetArgs = { agentId: agent.id, name, isClickable }; return `[${agent.name}](${agentRefUrl}?${encodeURIComponent(JSON.stringify(args))})`; } interface IAgentWidgetArgs { agentId: string; + name: string; isClickable?: boolean; } @@ -67,7 +78,6 @@ export class ChatMarkdownDecorationsRenderer { @IHoverService private readonly hoverService: IHoverService, @IChatService private readonly chatService: IChatService, @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, - @IChatAgentNameService private readonly chatAgentNameService: IChatAgentNameService, @ICommandService private readonly commandService: ICommandService, ) { } @@ -77,7 +87,7 @@ export class ChatMarkdownDecorationsRenderer { if (part instanceof ChatRequestTextPart) { result += part.text; } else if (part instanceof ChatRequestAgentPart) { - result += agentToMarkdown(part.agent, false); + result += this.instantiationService.invokeFunction(accessor => agentToMarkdown(part.agent, false, accessor)); } else { const uri = part instanceof ChatRequestDynamicVariablePart && part.data instanceof URI ? part.data : @@ -142,20 +152,7 @@ export class ChatMarkdownDecorationsRenderer { } private renderAgentWidget(args: IAgentWidgetArgs, store: DisposableStore): HTMLElement { - const agent = this.chatAgentService.getAgent(args.agentId); - let name: string; - if (agent) { - const isAllowed = this.chatAgentNameService.getAgentNameRestriction(agent).get(); - name = `${chatAgentLeader}${isAllowed ? agent.name : getFullyQualifiedId(agent)}`; - - const isDupe = isAllowed && this.chatAgentService.getAgentsByName(agent.name).length > 1; - if (isDupe) { - name += ` (${agent.publisherDisplayName})`; - } - } else { - name = args.agentId; - } - + const nameWithLeader = `${chatAgentLeader}${args.name}`; let container: HTMLElement; if (args.isClickable) { container = dom.$('span.chat-agent-widget'); @@ -164,8 +161,9 @@ export class ChatMarkdownDecorationsRenderer { buttonForeground: asCssVariable(chatSlashCommandForeground), buttonHoverBackground: undefined })); - button.label = name; + button.label = nameWithLeader; store.add(button.onDidClick(() => { + const agent = this.chatAgentService.getAgent(args.agentId); const widget = this.chatWidgetService.lastFocusedWidget; if (!widget || !agent) { return; @@ -174,9 +172,10 @@ export class ChatMarkdownDecorationsRenderer { this.chatService.sendRequest(widget.viewModel!.sessionId, agent.metadata.sampleRequest ?? '', { location: widget.location, agentId: agent.id }); })); } else { - container = this.renderResourceWidget(name, undefined); + container = this.renderResourceWidget(nameWithLeader, undefined); } + const agent = this.chatAgentService.getAgent(args.agentId); const hover: Lazy = new Lazy(() => store.add(this.instantiationService.createInstance(ChatAgentHover))); store.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('element'), container, () => { hover.value.setAgent(args.agentId); diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index 2fd60cfb727..906ec0914b3 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -335,6 +335,7 @@ export class MergedChatAgent implements IChatAgent { get id(): string { return this.data.id; } get name(): string { return this.data.name ?? ''; } + get fullName(): string { return this.data.fullName ?? ''; } get description(): string { return this.data.description ?? ''; } get extensionId(): ExtensionIdentifier { return this.data.extensionId; } get extensionPublisherId(): string { return this.data.extensionPublisherId; } diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index d4649b0acd7..dfab30c827b 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -956,7 +956,7 @@ export class ChatWelcomeMessageModel implements IChatWelcomeMessageModel { } public get username(): string { - return this.chatAgentService.getDefaultAgent(ChatAgentLocation.Panel)?.fullName ?? ''; + return this.chatAgentService.getContributedDefaultAgent(ChatAgentLocation.Panel)?.fullName ?? ''; } public get avatarIcon(): ThemeIcon | undefined { From 763d5f394d20376608cfc4274a9b6565d8af0aa5 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Fri, 10 May 2024 15:16:51 -0700 Subject: [PATCH 108/357] Only allow clicking a chat confirmation button a single time (#212476) * Only allow clicking a chat confirmation button a single time * Use new method in other places --- .../chat/browser/chatConfirmationWidget.ts | 5 +++ .../contrib/chat/browser/chatListRenderer.ts | 37 +++++++++++++------ .../contrib/chat/browser/media/chat.css | 24 ------------ .../browser/media/chatConfirmationWidget.css | 37 +++++++++++++++++++ .../contrib/chat/common/chatService.ts | 1 + 5 files changed, 69 insertions(+), 35 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/media/chatConfirmationWidget.css diff --git a/src/vs/workbench/contrib/chat/browser/chatConfirmationWidget.ts b/src/vs/workbench/contrib/chat/browser/chatConfirmationWidget.ts index 462cd0cb6ea..ea5cf39c113 100644 --- a/src/vs/workbench/contrib/chat/browser/chatConfirmationWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatConfirmationWidget.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; +import 'vs/css!./media/chatConfirmationWidget'; import { Button } from 'vs/base/browser/ui/button/button'; import { Emitter, Event } from 'vs/base/common/event'; import { MarkdownString } from 'vs/base/common/htmlContent'; @@ -27,6 +28,10 @@ export class ChatConfirmationWidget extends Disposable { return this._domNode; } + setShowButtons(showButton: boolean): void { + this.domNode.classList.toggle('hideButtons', !showButton); + } + constructor( title: string, message: string, diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index a81b3b1df73..1ea60017264 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -460,7 +460,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { - this._onDidChangeItemHeight.fire({ element, height: templateData.rowContainer.offsetHeight }); + this.updateItemHeight(templateData); })); treeDisposables.add(tree.onContextMenu((e) => { e.browserEvent.preventDefault(); @@ -731,7 +741,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { if (!ref.isStale()) { tree.layout(); - this._onDidChangeItemHeight.fire({ element, height: templateData.rowContainer.offsetHeight }); + this.updateItemHeight(templateData); } }); @@ -807,7 +817,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { + store.add(confirmationWidget.onDidClick(async e => { if (isResponseVM(element)) { const prompt = `${e.label}: "${confirmation.title}"`; const data: IChatSendRequestOptions = e.isSecondary ? { rejectedConfirmationData: [e.data] } : { acceptedConfirmationData: [e.data] }; data.agentId = element.agent?.id; - this.chatService.sendRequest(element.sessionId, prompt, data); + if (await this.chatService.sendRequest(element.sessionId, prompt, data)) { + confirmation.isUsed = true; + confirmationWidget.setShowButtons(false); + this.updateItemHeight(templateData); + } } })); @@ -973,7 +988,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { ref.object.layout(this._currentLayoutWidth); - this._onDidChangeItemHeight.fire({ element, height: templateData.rowContainer.offsetHeight }); + this.updateItemHeight(templateData); })); const data: ICodeCompareBlockData = { @@ -1075,7 +1090,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { ref.object.layout(this._currentLayoutWidth); - this._onDidChangeItemHeight.fire({ element, height: templateData.rowContainer.offsetHeight }); + this.updateItemHeight(templateData); })); if (isResponseVM(element)) { @@ -1096,7 +1111,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer this._onDidChangeItemHeight.fire({ element, height: templateData.rowContainer.offsetHeight }), + asyncRenderCallback: () => this.updateItemHeight(templateData), }); if (isResponseVM(element)) { diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 3b26978c868..337dc4d4bf6 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -711,27 +711,3 @@ margin-left: 0; margin-top: 1px; } - -.chat-confirmation-widget { - border: 1px solid var(--vscode-chat-requestBorder); - border-radius: 4px; - margin-bottom: 16px; - padding: 12px 16px 16px; -} - -.chat-confirmation-widget .chat-confirmation-widget-title { - font-weight: 600; -} - -.chat-confirmation-widget .chat-confirmation-widget-title p { - margin: 0 0 4px 0; -} - -.chat-confirmation-widget .chat-confirmation-widget-message .rendered-markdown p { - margin-top: 0; -} - -.chat-confirmation-widget .chat-confirmation-buttons-container { - display: flex; - gap: 8px; -} diff --git a/src/vs/workbench/contrib/chat/browser/media/chatConfirmationWidget.css b/src/vs/workbench/contrib/chat/browser/media/chatConfirmationWidget.css new file mode 100644 index 00000000000..48b493331b8 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/media/chatConfirmationWidget.css @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.chat-confirmation-widget { + border: 1px solid var(--vscode-chat-requestBorder); + border-radius: 4px; + margin-bottom: 16px; + padding: 12px 16px 16px; +} + +.chat-confirmation-widget .chat-confirmation-widget-title { + font-weight: 600; +} + +.chat-confirmation-widget .chat-confirmation-widget-title p { + margin: 0 0 4px 0; +} + +.chat-confirmation-widget .chat-confirmation-widget-message .rendered-markdown p { + margin-top: 0; +} + +.chat-confirmation-widget .chat-confirmation-widget-message .rendered-markdown > :last-child { + margin-bottom: 0px; +} + +.chat-confirmation-widget .chat-confirmation-buttons-container { + display: flex; + gap: 8px; + margin-top: 13px; +} + +.chat-confirmation-widget.hideButtons .chat-confirmation-buttons-container { + display: none; +} diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 3d60704721c..fbc7a65cf35 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -152,6 +152,7 @@ export interface IChatConfirmation { title: string; message: string; data: any; + isUsed?: boolean; kind: 'confirmation'; } From 351fa19d43d6415a13a2a1dec29a165d5fa5f5c6 Mon Sep 17 00:00:00 2001 From: Aaron Munger Date: Fri, 10 May 2024 15:29:49 -0700 Subject: [PATCH 109/357] dont bump version if nothing changes (#212478) --- .../common/model/notebookTextModel.ts | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index e8ca556b48d..9ee44dcf0c7 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -146,6 +146,10 @@ type TransformedEdit = { }; class NotebookEventEmitter extends PauseableEmitter { + get isEmpty() { + return this._eventQueue.isEmpty(); + } + isDirtyEvent() { for (const e of this._eventQueue) { for (let i = 0; i < e.rawEvents.length; i++) { @@ -513,16 +517,18 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this._doApplyEdits(rawEdits, synchronous, computeUndoRedo, beginSelectionState, undoRedoGroup); return true; } finally { - // Update selection and versionId after applying edits. - const endSelections = endSelectionsComputer(); - this._increaseVersionId(this._operationManager.isUndoStackEmpty() && !this._pauseableEmitter.isDirtyEvent()); + if (!this._pauseableEmitter.isEmpty) { + // Update selection and versionId after applying edits. + const endSelections = endSelectionsComputer(); + this._increaseVersionId(this._operationManager.isUndoStackEmpty() && !this._pauseableEmitter.isDirtyEvent()); - // Finalize undo element - this._operationManager.pushStackElement(this._alternativeVersionId, endSelections); + // Finalize undo element + this._operationManager.pushStackElement(this._alternativeVersionId, endSelections); - // Broadcast changes - this._pauseableEmitter.fire({ rawEvents: [], versionId: this.versionId, synchronous: synchronous, endSelectionState: endSelections }); - this._pauseableEmitter.resume(); + // Broadcast changes + this._pauseableEmitter.fire({ rawEvents: [], versionId: this.versionId, synchronous: synchronous, endSelectionState: endSelections }); + this._pauseableEmitter.resume(); + } } } From bbc0159f43111d44f6cd0cf8909a5a1fdf396ab7 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Fri, 10 May 2024 18:02:08 -0700 Subject: [PATCH 110/357] feat: associate chat references and warnings with chat progress (#212391) feat: associate chat references and warnings with chat progress --- .../api/browser/mainThreadChatAgents2.ts | 71 +++++++++++++---- .../api/common/extHostChatAgents2.ts | 28 +++++-- src/vs/workbench/api/common/extHostTypes.ts | 4 +- .../contrib/chat/browser/chatListRenderer.ts | 77 +++++++++++++------ .../contrib/chat/browser/media/chat.css | 12 +++ .../contrib/chat/common/chatModel.ts | 8 ++ .../contrib/chat/common/chatService.ts | 7 ++ .../contrib/chat/common/chatViewModel.ts | 1 + ...ode.proposed.chatParticipantAdditions.d.ts | 6 +- 9 files changed, 162 insertions(+), 52 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index d8e616d224a..ff489e53f35 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -5,6 +5,8 @@ import { DeferredPromise } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { Emitter, Event } from 'vs/base/common/event'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; import { Disposable, DisposableMap, IDisposable } from 'vs/base/common/lifecycle'; import { revive } from 'vs/base/common/marshalling'; import { escapeRegExpCharacters } from 'vs/base/common/strings'; @@ -25,7 +27,7 @@ import { AddDynamicVariableAction, IAddDynamicVariableContext } from 'vs/workben import { ChatAgentLocation, IChatAgentImplementation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; 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 { IChatContentReference, IChatFollowup, IChatProgress, IChatService, IChatTask, IChatWarningMessage } from 'vs/workbench/contrib/chat/common/chatService'; import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -36,6 +38,36 @@ interface AgentData { hasFollowups?: boolean; } +class MainThreadChatTask implements IChatTask { + public readonly kind = 'progressTask'; + + public readonly deferred = new DeferredPromise(); + + private readonly _onDidAddProgress = new Emitter(); + public get onDidAddProgress(): Event { return this._onDidAddProgress.event; } + + public readonly progress: (IChatWarningMessage | IChatContentReference)[] = []; + + constructor(public content: IMarkdownString) { } + + task() { + return this.deferred.p; + } + + isSettled() { + return this.deferred.isSettled; + } + + complete(v: string | void) { + this.deferred.complete(v); + } + + add(progress: IChatWarningMessage | IChatContentReference): void { + this.progress.push(progress); + this._onDidAddProgress.fire(progress); + } +} + @extHostNamedCustomer(MainContext.MainThreadChatAgents2) export class MainThreadChatAgents2 extends Disposable implements MainThreadChatAgentsShape2 { @@ -46,7 +78,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA private readonly _proxy: ExtHostChatAgentsShape2; private _responsePartHandlePool = 0; - private readonly _activeResponsePartPromises = new Map>(); + private readonly _activeTasks = new Map(); constructor( extHostContext: IExtHostContext, @@ -172,26 +204,33 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA } async $handleProgressChunk(requestId: string, progress: IChatProgressDto, responsePartHandle?: number): Promise { - if (progress.kind === 'progressTask') { + const revivedProgress = revive(progress) as IChatProgress; + if (revivedProgress.kind === 'progressTask') { const handle = ++this._responsePartHandlePool; const responsePartId = `${requestId}_${handle}`; - const deferredContentPromise = new DeferredPromise(); - this._activeResponsePartPromises.set(responsePartId, deferredContentPromise); - this._pendingProgress.get(requestId)?.({ ...progress, task: () => deferredContentPromise.p, isSettled: () => deferredContentPromise.isSettled }); + const task = new MainThreadChatTask(revivedProgress.content); + this._activeTasks.set(responsePartId, task); + this._pendingProgress.get(requestId)?.(task); return handle; - } else if (progress.kind === 'progressTaskResult' && responsePartHandle !== undefined) { + } else if (responsePartHandle !== undefined) { const responsePartId = `${requestId}_${responsePartHandle}`; - const deferredContentPromise = this._activeResponsePartPromises.get(responsePartId); - if (deferredContentPromise && progress.content) { - deferredContentPromise.complete(progress.content.value); - this._activeResponsePartPromises.delete(responsePartId); - } else { - deferredContentPromise?.complete(undefined); + const task = this._activeTasks.get(responsePartId); + switch (revivedProgress.kind) { + case 'progressTaskResult': + if (task && revivedProgress.content) { + task.complete(revivedProgress.content.value); + this._activeTasks.delete(responsePartId); + } else { + task?.complete(undefined); + } + return responsePartHandle; + case 'warning': + case 'reference': + task?.add(revivedProgress); + return; } - return responsePartHandle; } - const revivedProgress = revive(progress); - this._pendingProgress.get(requestId)?.(revivedProgress as IChatProgress); + this._pendingProgress.get(requestId)?.(revivedProgress); } $registerAgentCompletionsProvider(handle: number, triggerCharacters: string[]): void { diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 11fcd8875c6..e80ccfa7e61 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -68,17 +68,31 @@ class ChatAgentResponseStream { } } - const _report = (progress: IChatProgressDto, task?: () => Thenable) => { + const _report = (progress: IChatProgressDto, task?: (progress: vscode.Progress) => Thenable) => { // Measure the time to the first progress update with real markdown content if (typeof this._firstProgress === 'undefined' && 'content' in progress) { this._firstProgress = this._stopWatch.elapsed(); } - Promise.all([this._proxy.$handleProgressChunk(this._request.requestId, progress), task ? task() : undefined]).then(([handle, res]) => { - if (typeof handle === 'number' && task) { - this._proxy.$handleProgressChunk(this._request.requestId, typeConvert.ChatTaskResult.from(res), handle); - } - }); + this._proxy.$handleProgressChunk(this._request.requestId, progress) + .then((handle) => { + if (handle) { + task?.({ + report: (p) => { + if (extHostTypes.MarkdownString.isMarkdownString(p.value)) { + this._proxy.$handleProgressChunk(this._request.requestId, typeConvert.ChatResponseWarningPart.from(p), handle); + return; + } else { + this._proxy.$handleProgressChunk(this._request.requestId, typeConvert.ChatResponseReferencePart.from(p), handle); + } + } + }).then((res) => { + if (typeof handle === 'number') { + this._proxy.$handleProgressChunk(this._request.requestId, typeConvert.ChatTaskResult.from(res), handle); + } + }); + } + }); }; this._apiObject = { @@ -121,7 +135,7 @@ class ChatAgentResponseStream { _report(dto); return this; }, - progress(value, task?: (() => Thenable)) { + progress(value, task?: ((progress: vscode.Progress) => Thenable)) { throwIfDone(this.progress); const part = new extHostTypes.ChatResponseProgressPart2(value, task); const dto = task ? typeConvert.ChatTask.from(part) : typeConvert.ChatResponseProgressPart.from(part); diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index a21e39c192b..e0dc6404965 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -4376,8 +4376,8 @@ export class ChatResponseProgressPart { export class ChatResponseProgressPart2 { value: string; - task?: () => Thenable; - constructor(value: string, task?: () => Thenable) { + task?: (progress: vscode.Progress) => Thenable; + constructor(value: string, task?: (progress: vscode.Progress) => Thenable) { this.value = value; this.task = task; } diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 1ea60017264..57d28868516 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -456,7 +456,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer !('isSettled' in p) || !p.isSettled).length === 0 && !somePartIsNotFullyRendered; if (isFullyRendered && element.isComplete) { // Response is done and content is rendered, so do a normal render @@ -662,7 +663,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer, element: IChatResponseViewModel, templateData: IChatListItemTemplate): { element: HTMLElement; dispose: () => void } { + private renderContentReferencesListData(task: IChatTask | null, data: ReadonlyArray, element: IChatResponseViewModel, templateData: IChatListItemTemplate): { element: HTMLElement; dispose: () => void } { const listDisposables = new DisposableStore(); - const referencesLabel = data.length > 1 ? + const referencesLabel = task?.content.value ?? (data.length > 1 ? localize('usedReferencesPlural', "Used {0} references", data.length) : - localize('usedReferencesSingular', "Used {0} reference", 1); + localize('usedReferencesSingular', "Used {0} reference", 1)); const iconElement = $('.chat-used-context-icon'); const icon = (element: IChatResponseViewModel) => element.usedReferencesExpanded ? Codicon.chevronDown : Codicon.chevronRight; iconElement.classList.add(...ThemeIcon.asClassNameArray(icon(element))); @@ -826,7 +827,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { - if (e.element) { + if (e.element && 'reference' in e.element) { const uriOrLocation = 'variableName' in e.element.reference ? e.element.reference.value : e.element.reference; const uri = URI.isUri(uriOrLocation) ? uriOrLocation : uriOrLocation?.uri; @@ -869,6 +870,21 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer>; + private _pool: ResourcePool>; - public get inUse(): ReadonlySet> { + public get inUse(): ReadonlySet> { return this._pool.inUse; } @@ -1353,14 +1369,14 @@ class ContentReferencesListPool extends Disposable { this._pool = this._register(new ResourcePool(() => this.listFactory())); } - private listFactory(): WorkbenchList { + private listFactory(): WorkbenchList { const resourceLabels = this._register(this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility })); const container = $('.chat-used-context-list'); this._register(createFileIconThemableTreeContainerScope(container, this.themeService)); const list = this.instantiationService.createInstance( - WorkbenchList, + WorkbenchList, 'ChatListRenderer', container, new ContentReferencesListDelegate(), @@ -1368,7 +1384,10 @@ class ContentReferencesListPool extends Disposable { { alwaysConsumeMouseWheel: false, accessibilityProvider: { - getAriaLabel: (element: IChatContentReference) => { + getAriaLabel: (element: IChatContentReference | IChatWarningMessage) => { + if (element.kind === 'warning') { + return element.content.value; + } const reference = element.reference; if ('variableName' in reference) { return reference.variableName; @@ -1382,7 +1401,11 @@ class ContentReferencesListPool extends Disposable { getWidgetAriaLabel: () => localize('usedReferences', "Used References") }, dnd: { - getDragURI: ({ reference }: IChatContentReference) => { + getDragURI: (element: IChatContentReference | IChatWarningMessage) => { + if (element.kind === 'warning') { + return null; + } + const { reference } = element; if ('variableName' in reference) { return null; } else if (URI.isUri(reference)) { @@ -1400,7 +1423,7 @@ class ContentReferencesListPool extends Disposable { return list; } - get(): IDisposableReference> { + get(): IDisposableReference> { const object = this._pool.get(); let stale = false; return { @@ -1414,7 +1437,7 @@ class ContentReferencesListPool extends Disposable { } } -class ContentReferencesListDelegate implements IListVirtualDelegate { +class ContentReferencesListDelegate implements IListVirtualDelegate { getHeight(element: IChatContentReference): number { return 22; } @@ -1429,7 +1452,7 @@ interface IChatContentReferenceListTemplate { templateDisposables: IDisposable; } -class ContentReferencesListRenderer implements IListRenderer { +class ContentReferencesListRenderer implements IListRenderer { static TEMPLATE_ID = 'contentReferencesListRenderer'; readonly templateId: string = ContentReferencesListRenderer.TEMPLATE_ID; @@ -1450,12 +1473,18 @@ class ContentReferencesListRenderer implements IListRenderer { + this._updateRepr(false); + }); + progress.task?.().then((content) => { + // Stop listening for progress updates once the task settles + disp.dispose(); + // Replace the resolving part's content with the resolved response if (typeof content === 'string') { this._responseParts[responsePosition] = { ...progress, content: new MarkdownString(content) }; } this._updateRepr(false); }); + } else { this._responseParts.push(progress); this._updateRepr(quiet); diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index fbc7a65cf35..15cfb2b6c87 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { DeferredPromise } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; import { IMarkdownString } from 'vs/base/common/htmlContent'; @@ -107,6 +108,12 @@ export interface IChatProgressMessage { } export interface IChatTask extends IChatTaskDto { + deferred: DeferredPromise; + progress: (IChatWarningMessage | IChatContentReference)[]; + onDidAddProgress: Event; + add(progress: IChatWarningMessage | IChatContentReference): void; + + complete: (result: string | void) => void; task: () => Promise; isSettled: () => boolean; } diff --git a/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/chatViewModel.ts index 96b4ef6d464..17087c00d65 100644 --- a/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatViewModel.ts @@ -98,6 +98,7 @@ export interface IChatProgressMessageRenderData { export interface IChatTaskRenderData { task: IChatTask; isSettled: boolean; + progressLength: number; } export type IChatRenderData = IChatResponseProgressFileTreeData | IChatResponseMarkdownRenderData | IChatProgressMessageRenderData | IChatCommandButton | IChatTextEditGroup | IChatConfirmation | IChatTaskRenderData | IChatWarningMessage; diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index 875849bc8ed..1f0d3796885 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -118,8 +118,8 @@ declare module 'vscode' { export class ChatResponseProgressPart2 extends ChatResponseProgressPart { value: string; - task?: () => Thenable; - constructor(value: string, task?: () => Thenable); + task?: (progress: Progress) => Thenable; + constructor(value: string, task?: (progress: Progress) => Thenable); } export interface ChatResponseStream { @@ -132,7 +132,7 @@ declare module 'vscode' { * @param task If provided, a task to run while the progress is displayed. When the Thenable resolves, the progress will be marked complete in the UI, and the progress message will be updated to the resolved string if one is specified. * @returns This stream. */ - progress(value: string, task?: () => Thenable): ChatResponseStream; + progress(value: string, task?: (progress: Progress) => Thenable): ChatResponseStream; textEdit(target: Uri, edits: TextEdit | TextEdit[]): ChatResponseStream; markdownWithVulnerabilities(value: string | MarkdownString, vulnerabilities: ChatVulnerability[]): ChatResponseStream; From 9d4274e5599f6ba2c667a54ec0d1d6594d30145b Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Sat, 11 May 2024 05:49:03 -0700 Subject: [PATCH 111/357] Remove session if it is being replaced (#212504) A bug that has probably existed for quite a while... if we are replacing a session, we should say the old session is removed. --- extensions/github-authentication/src/github.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/extensions/github-authentication/src/github.ts b/extensions/github-authentication/src/github.ts index c603455facf..15fe2ef04f8 100644 --- a/extensions/github-authentication/src/github.ts +++ b/extensions/github-authentication/src/github.ts @@ -306,14 +306,15 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid this.afterSessionLoad(session); const sessionIndex = sessions.findIndex(s => s.id === session.id || arrayEquals([...s.scopes].sort(), sortedScopes)); + const removed = new Array(); if (sessionIndex > -1) { - sessions.splice(sessionIndex, 1, session); + removed.push(...sessions.splice(sessionIndex, 1, session)); } else { sessions.push(session); } await this.storeSessions(sessions); - this._sessionChangeEmitter.fire({ added: [session], removed: [], changed: [] }); + this._sessionChangeEmitter.fire({ added: [session], removed, changed: [] }); this._logger.info('Login success!'); From 54edfb7675200c441099515cde2b960c668012fd Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Sun, 12 May 2024 07:34:40 +0200 Subject: [PATCH 112/357] update distro (#212532) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 51196b2b6a7..08597898b27 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.90.0", - "distro": "fdf8ae1d8f85249891bd9670ac010fae9162eecc", + "distro": "b545c05b2646e72d172e58e08b9315e5e8478296", "author": { "name": "Microsoft Corporation" }, From 29aeab1cbb350107a7bd5962b5e7efe745e0a3ec Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Sun, 12 May 2024 16:55:23 -0700 Subject: [PATCH 113/357] Include chat agent ID in telemetry (#212516) --- .../workbench/api/common/extHost.protocol.ts | 4 +-- .../api/common/extHostChatAgents2.ts | 8 +++--- .../chat/browser/actions/chatTitleActions.ts | 10 +++---- .../contrib/chat/browser/chatListRenderer.ts | 4 +-- .../contrib/chat/common/chatModel.ts | 14 +++++----- .../contrib/chat/common/chatService.ts | 5 ++-- .../contrib/chat/common/chatServiceImpl.ts | 26 ++++++++++++++----- .../contrib/chat/common/chatViewModel.ts | 8 +++--- .../browser/inlineChatSessionServiceImpl.ts | 4 +-- .../chat/browser/terminalChatController.ts | 6 ++--- 10 files changed, 50 insertions(+), 39 deletions(-) diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index a8750532975..d7e6a87fb35 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -52,7 +52,7 @@ import { IRevealOptions, ITreeItem, IViewBadge } from 'vs/workbench/common/views import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; import { ChatAgentLocation, IChatAgentMetadata, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatProgressResponseContent } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IChatFollowup, IChatProgress, IChatResponseErrorDetails, IChatTask, IChatTaskDto, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatFollowup, IChatProgress, IChatResponseErrorDetails, IChatTask, IChatTaskDto, IChatUserActionEvent, ChatAgentVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatRequestVariableValue, IChatVariableData, IChatVariableResolverProgress } from 'vs/workbench/contrib/chat/common/chatVariables'; import { IChatMessage, IChatResponseFragment, ILanguageModelChatMetadata, ILanguageModelChatSelector, ILanguageModelsChangeEvent } from 'vs/workbench/contrib/chat/common/languageModels'; import { DebugConfigurationProviderTriggerKind, IAdapterDescriptor, IConfig, IDebugSessionReplMode, IDebugVisualization, IDebugVisualizationContext, IDebugVisualizationTreeItem, MainThreadDebugVisualization } from 'vs/workbench/contrib/debug/common/debug'; @@ -1272,7 +1272,7 @@ export type IChatAgentHistoryEntryDto = { export interface ExtHostChatAgentsShape2 { $invokeAgent(handle: number, request: IChatAgentRequest, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise; $provideFollowups(request: IChatAgentRequest, handle: number, result: IChatAgentResult, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise; - $acceptFeedback(handle: number, result: IChatAgentResult, vote: InteractiveSessionVoteDirection, reportIssue?: boolean): void; + $acceptFeedback(handle: number, result: IChatAgentResult, vote: ChatAgentVoteDirection, reportIssue?: boolean): void; $acceptAction(handle: number, result: IChatAgentResult, action: IChatUserActionEvent): void; $invokeCompletionProvider(handle: number, query: string, token: CancellationToken): Promise; $provideWelcomeMessage(handle: number, location: ChatAgentLocation, token: CancellationToken): Promise<(string | IMarkdownString)[] | undefined>; diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index e80ccfa7e61..0d3c964220d 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -22,7 +22,7 @@ import { CommandsConverter, ExtHostCommands } from 'vs/workbench/api/common/extH import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; import { ChatAgentLocation, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { IChatContentReference, IChatFollowup, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatContentReference, IChatFollowup, IChatUserActionEvent, ChatAgentVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { checkProposedApiEnabled, isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import { Dto } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import type * as vscode from 'vscode'; @@ -373,7 +373,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS .map(f => typeConvert.ChatFollowup.from(f, request)); } - $acceptFeedback(handle: number, result: IChatAgentResult, vote: InteractiveSessionVoteDirection, reportIssue?: boolean): void { + $acceptFeedback(handle: number, result: IChatAgentResult, vote: ChatAgentVoteDirection, reportIssue?: boolean): void { const agent = this._agents.get(handle); if (!agent) { return; @@ -382,10 +382,10 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS const ehResult = typeConvert.ChatAgentResult.to(result); let kind: extHostTypes.ChatResultFeedbackKind; switch (vote) { - case InteractiveSessionVoteDirection.Down: + case ChatAgentVoteDirection.Down: kind = extHostTypes.ChatResultFeedbackKind.Unhelpful; break; - case InteractiveSessionVoteDirection.Up: + case ChatAgentVoteDirection.Up: kind = extHostTypes.ChatResultFeedbackKind.Helpful; break; } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts index 8baf92e0312..784bdcacf66 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts @@ -17,7 +17,7 @@ import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatAct import { ChatTreeItem, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; import { CONTEXT_CHAT_LOCATION, CONTEXT_CHAT_RESPONSE_SUPPORT_ISSUE_REPORTING, CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION, CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_DETECTED_AGENT_COMMAND, CONTEXT_RESPONSE_FILTERED, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys'; -import { IChatService, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatService, ChatAgentVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { isRequestVM, isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellEditType, CellKind, NOTEBOOK_EDITOR_ID } from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -57,10 +57,10 @@ export function registerChatTitleActions() { result: item.result, action: { kind: 'vote', - direction: InteractiveSessionVoteDirection.Up, + direction: ChatAgentVoteDirection.Up, } }); - item.setVote(InteractiveSessionVoteDirection.Up); + item.setVote(ChatAgentVoteDirection.Up); } }); @@ -96,10 +96,10 @@ export function registerChatTitleActions() { result: item.result, action: { kind: 'vote', - direction: InteractiveSessionVoteDirection.Down, + direction: ChatAgentVoteDirection.Down, } }); - item.setVote(InteractiveSessionVoteDirection.Down); + item.setVote(ChatAgentVoteDirection.Down); } }); diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 57d28868516..c1a02143150 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -68,7 +68,7 @@ import { ChatAgentLocation, IChatAgentMetadata } from 'vs/workbench/contrib/chat import { CONTEXT_CHAT_RESPONSE_SUPPORT_ISSUE_REPORTING, CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_DETECTED_AGENT_COMMAND, CONTEXT_RESPONSE_FILTERED, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatProgressRenderableResponseContent, IChatTextEditGroup } from 'vs/workbench/contrib/chat/common/chatModel'; import { chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { IChatCommandButton, IChatConfirmation, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseProgressFileTreeData, IChatSendRequestOptions, IChatService, IChatTask, IChatWarningMessage, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatCommandButton, IChatConfirmation, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseProgressFileTreeData, IChatSendRequestOptions, IChatService, IChatTask, IChatWarningMessage, ChatAgentVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import { IChatProgressMessageRenderData, IChatRenderData, IChatResponseMarkdownRenderData, IChatResponseViewModel, IChatTaskRenderData, IChatWelcomeMessageViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { IWordCountResult, getNWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; @@ -325,7 +325,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer ) { @@ -415,7 +415,7 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel this._onDidChange.fire(); // Fire so that command followups get rendered on the row } - setVote(vote: InteractiveSessionVoteDirection): void { + setVote(vote: ChatAgentVoteDirection): void { this._vote = vote; this._onDidChange.fire(); } @@ -465,7 +465,7 @@ export interface ISerializableChatRequestData { result?: IChatAgentResult; // Optional for backcompat followups: ReadonlyArray | undefined; isCanceled: boolean | undefined; - vote: InteractiveSessionVoteDirection | undefined; + vote: ChatAgentVoteDirection | undefined; /** For backward compat: should be optional */ usedContext?: IChatUsedContext; contentReferences?: ReadonlyArray; diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 15cfb2b6c87..4f3b58978cd 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -188,15 +188,14 @@ export interface IChatFollowup { tooltip?: string; } -// Name has to match the one in vscode.d.ts for some reason -export enum InteractiveSessionVoteDirection { +export enum ChatAgentVoteDirection { Down = 0, Up = 1 } export interface IChatVoteAction { kind: 'vote'; - direction: InteractiveSessionVoteDirection; + direction: ChatAgentVoteDirection; reportIssue?: boolean; } diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index 034ad834957..426e52d00f7 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 { ChatModel, ChatRequestModel, ChatWelcomeMessageModel, IChatModel, IChatRequestModel, IChatRequestVariableData, IChatRequestVariableEntry, IExportableChatData, ISerializableChatData, ISerializableChatsData, getHistoryEntriesFromModel, updateRanges } from 'vs/workbench/contrib/chat/common/chatModel'; import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, IParsedChatRequest, chatAgentLeader, chatSubcommandLeader, getPromptText } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; -import { ChatCopyKind, IChatCompleteResponse, IChatDetail, IChatFollowup, IChatProgress, IChatSendRequestData, IChatSendRequestOptions, IChatService, IChatTransferredSessionData, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { ChatCopyKind, IChatCompleteResponse, IChatDetail, IChatFollowup, IChatProgress, IChatSendRequestData, IChatSendRequestOptions, IChatService, IChatTransferredSessionData, IChatUserActionEvent, ChatAgentVoteDirection } 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 { ChatMessageRole, IChatMessage } from 'vs/workbench/contrib/chat/common/languageModels'; @@ -61,45 +61,53 @@ type ChatProviderInvokedClassification = { agent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of agent used.' }; slashCommand?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of slashCommand used.' }; owner: 'roblourens'; - comment: 'Provides insight into the performance of Chat providers.'; + comment: 'Provides insight into the performance of Chat agents.'; }; type ChatVoteEvent = { direction: 'up' | 'down'; + agentId: string; }; type ChatVoteClassification = { direction: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the user voted up or down.' }; + agentId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the chat agent that this vote is for.' }; owner: 'roblourens'; - comment: 'Provides insight into the performance of Chat providers.'; + comment: 'Provides insight into the performance of Chat agents.'; }; type ChatCopyEvent = { copyKind: 'action' | 'toolbar'; + agentId: string; }; type ChatCopyClassification = { copyKind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'How the copy was initiated.' }; + agentId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the chat agent that the copy acted on.' }; owner: 'roblourens'; comment: 'Provides insight into the usage of Chat features.'; }; type ChatInsertEvent = { newFile: boolean; + agentId: string; }; type ChatInsertClassification = { newFile: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the code was inserted into a new untitled file.' }; + agentId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the chat agent that this insertion is for.' }; owner: 'roblourens'; comment: 'Provides insight into the usage of Chat features.'; }; type ChatCommandEvent = { commandId: string; + agentId: string; }; type ChatCommandClassification = { commandId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The id of the command that was executed.' }; + agentId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the related chat agent.' }; owner: 'roblourens'; comment: 'Provides insight into the usage of Chat features.'; }; @@ -207,22 +215,26 @@ export class ChatService extends Disposable implements IChatService { notifyUserAction(action: IChatUserActionEvent): void { if (action.action.kind === 'vote') { this.telemetryService.publicLog2('interactiveSessionVote', { - direction: action.action.direction === InteractiveSessionVoteDirection.Up ? 'up' : 'down' + direction: action.action.direction === ChatAgentVoteDirection.Up ? 'up' : 'down', + agentId: action.agentId ?? '' }); } else if (action.action.kind === 'copy') { this.telemetryService.publicLog2('interactiveSessionCopy', { - copyKind: action.action.copyKind === ChatCopyKind.Action ? 'action' : 'toolbar' + copyKind: action.action.copyKind === ChatCopyKind.Action ? 'action' : 'toolbar', + agentId: action.agentId ?? '' }); } else if (action.action.kind === 'insert') { this.telemetryService.publicLog2('interactiveSessionInsert', { - newFile: !!action.action.newFile + newFile: !!action.action.newFile, + agentId: action.agentId ?? '' }); } else if (action.action.kind === 'command') { // TODO not currently called const command = CommandsRegistry.getCommand(action.action.commandButton.command.id); const commandId = command ? action.action.commandButton.command.id : 'INVALID'; this.telemetryService.publicLog2('interactiveSessionCommand', { - commandId + commandId, + agentId: action.agentId ?? '' }); } else if (action.action.kind === 'runInTerminal') { this.telemetryService.publicLog2('interactiveSessionRunInTerminal', { diff --git a/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/chatViewModel.ts index 17087c00d65..ee9204379d2 100644 --- a/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatViewModel.ts @@ -14,7 +14,7 @@ import { annotateVulnerabilitiesInText } from 'vs/workbench/contrib/chat/common/ import { IChatAgentCommand, IChatAgentData, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatModelInitState, IChatModel, IChatRequestModel, IChatResponseModel, IChatTextEditGroup, IChatWelcomeMessageContent, IResponse } from 'vs/workbench/contrib/chat/common/chatModel'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { IChatCommandButton, IChatConfirmation, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseErrorDetails, IChatResponseProgressFileTreeData, IChatTask, IChatUsedContext, IChatWarningMessage, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatCommandButton, IChatConfirmation, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseErrorDetails, IChatResponseProgressFileTreeData, IChatTask, IChatUsedContext, IChatWarningMessage, ChatAgentVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; import { CodeBlockModelCollection } from './codeBlockModelCollection'; import { IMarkdownString } from 'vs/base/common/htmlContent'; @@ -132,14 +132,14 @@ export interface IChatResponseViewModel { readonly isComplete: boolean; readonly isCanceled: boolean; readonly isStale: boolean; - readonly vote: InteractiveSessionVoteDirection | undefined; + readonly vote: ChatAgentVoteDirection | undefined; readonly replyFollowups?: IChatFollowup[]; readonly errorDetails?: IChatResponseErrorDetails; readonly result?: IChatAgentResult; readonly contentUpdateTimings?: IChatLiveUpdateData; renderData?: IChatResponseRenderData; currentRenderedHeight: number | undefined; - setVote(vote: InteractiveSessionVoteDirection): void; + setVote(vote: ChatAgentVoteDirection): void; usedReferencesExpanded?: boolean; vulnerabilitiesListExpanded: boolean; setEditApplied(edit: IChatTextEditGroup, editCount: number): void; @@ -518,7 +518,7 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi this.logService.trace(`ChatResponseViewModel#${tag}: ${message}`); } - setVote(vote: InteractiveSessionVoteDirection): void { + setVote(vote: ChatAgentVoteDirection): void { this._modelChangeCount++; this._model.setVote(vote); } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts index 83a2621d73a..d6e7b0c9789 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts @@ -28,7 +28,7 @@ import { IProgress, Progress } from 'vs/platform/progress/common/progress'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { DEFAULT_EDITOR_ASSOCIATION } from 'vs/workbench/common/editor'; import { ChatAgentLocation, IChatAgent, IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentImplementation, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { IChatFollowup, IChatProgress, IChatService, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatFollowup, IChatProgress, IChatService, ChatAgentVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { EditMode, IInlineChatBulkEditResponse, IInlineChatProgressItem, IInlineChatRequest, IInlineChatResponse, IInlineChatService, IInlineChatSession, IInlineChatSessionProvider, IInlineChatSlashCommand, InlineChatResponseFeedbackKind, InlineChatResponseType } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; @@ -566,7 +566,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { let kind: InlineChatResponseFeedbackKind | undefined; if (e.action.kind === 'vote') { - kind = e.action.direction === InteractiveSessionVoteDirection.Down ? InlineChatResponseFeedbackKind.Unhelpful : InlineChatResponseFeedbackKind.Helpful; + kind = e.action.direction === ChatAgentVoteDirection.Down ? InlineChatResponseFeedbackKind.Unhelpful : InlineChatResponseFeedbackKind.Helpful; } else if (e.action.kind === 'bug') { kind = InlineChatResponseFeedbackKind.Bug; } else if (e.action.kind === 'inlineChat') { diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index ce8c9103c34..34274cf1ee2 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -13,7 +13,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { GeneratingPhrase, IChatAccessibilityService, IChatCodeBlockContextProviderService, showChatView } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatAgentLocation, IChatAgentRequest, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { ChatUserAction, IChatProgress, IChatService, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { ChatUserAction, IChatProgress, IChatService, ChatAgentVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { ITerminalContribution, ITerminalInstance, ITerminalService, IXtermTerminal, isDetachedTerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager'; import { ITerminalProcessManager } from 'vs/workbench/contrib/terminal/common/terminal'; @@ -136,7 +136,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr if (e.action.kind === 'bug') { this.acceptFeedback(undefined); } else if (e.action.kind === 'vote') { - this.acceptFeedback(e.action.direction === InteractiveSessionVoteDirection.Up); + this.acceptFeedback(e.action.direction === ChatAgentVoteDirection.Up); } } })); @@ -183,7 +183,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr action = { kind: 'bug' }; } else { this._sessionResponseVoteContextKey.set(helpful ? 'up' : 'down'); - action = { kind: 'vote', direction: helpful ? InteractiveSessionVoteDirection.Up : InteractiveSessionVoteDirection.Down }; + action = { kind: 'vote', direction: helpful ? ChatAgentVoteDirection.Up : ChatAgentVoteDirection.Down }; } // TODO:extract into helper method for (const request of model.getRequests()) { From 1a452916ac4842d655bc5815d656234d9a68cc8d Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Mon, 13 May 2024 10:25:29 +0200 Subject: [PATCH 114/357] Unthemable product icons #2 (#212574) --- src/vs/workbench/browser/media/style.css | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/browser/media/style.css b/src/vs/workbench/browser/media/style.css index 8d4313f5f15..35f856b931a 100644 --- a/src/vs/workbench/browser/media/style.css +++ b/src/vs/workbench/browser/media/style.css @@ -166,12 +166,15 @@ body.web { } .monaco-workbench .predefined-file-icon[class*='codicon-']::before { - font-family: 'codicon'; width: 16px; padding-left: 3px; /* width (16px) - font-size (13px) = padding-left (3px) */ padding-right: 3px; } +.predefined-file-icon::before { /* do add additional specificity to this selector, so it can be overridden by product themes */ + font-family: 'codicon'; +} + .monaco-workbench:not(.file-icons-enabled) .predefined-file-icon[class*='codicon-']::before { content: unset !important; } @@ -198,7 +201,6 @@ body.web { .monaco-workbench .select-container:after { content: var(--vscode-icon-chevron-down-content); font-family: var(--vscode-icon-chevron-down-font-family); - font-family: codicon; font-size: 16px; width: 16px; height: 16px; From b3f2d61be090f67ad48820aae6c13ac9d3a28ffa Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 13 May 2024 10:30:00 +0200 Subject: [PATCH 115/357] jsdoc updates (#212576) --- .../vscode.proposed.languageModels.d.ts | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/vscode-dts/vscode.proposed.languageModels.d.ts b/src/vscode-dts/vscode.proposed.languageModels.d.ts index 1e4782e366d..85ee736cb6f 100644 --- a/src/vscode-dts/vscode.proposed.languageModels.d.ts +++ b/src/vscode-dts/vscode.proposed.languageModels.d.ts @@ -93,6 +93,11 @@ declare module 'vscode' { constructor(role: LanguageModelChatMessageRole, content: string, name?: string); } + /** + * Represents a language model for making chat requests. + * + * @see {@link lm.selectChatModels} + */ // TODO@API name LanguageModelChatEndpoint export interface LanguageModelChat { /** @@ -102,18 +107,21 @@ declare module 'vscode' { /** * A well-know identifier of the vendor of the language model, a sample is `copilot`, but - * values are defined by extensions contributing chat model and need to be looked up with them. + * values are defined by extensions contributing chat models and need to be looked up with them. */ readonly vendor: string; + /** * Human-readable name of the language model. */ readonly name: string; + /** * Opaque family-name of the language model. Values might be `gpt-3.5-turbo`, `gpt4`, `phi2`, or `llama` * but they are defined by extensions contributing languages and subject to change. */ readonly family: string; + /** * Opaque version string of the model. This is defined by the extension contributing the language model * and subject to change while the identifier is stable. @@ -129,15 +137,11 @@ declare module 'vscode' { /** * Make a chat request using a language model. * - * - *Note 1:* language model use may be subject to access restrictions and user consent. Calling this function + * *Note* that language model use may be subject to access restrictions and user consent. Calling this function * for the first time (for a extension) will show a consent dialog to the user and because of that this function * must _only be called in response to a user action!_ Extension can use {@link LanguageModelAccessInformation.canSendRequest} * to check if they have the necessary permissions to make a request. * - * - *Note 2:* language models are contributed by other extensions and as they evolve and change, - * the set of available language models may change over time. Therefore it is strongly recommend to check - * {@link languageModels} for available values and handle missing language models gracefully. - * * This function will return a rejected promise if making a request to the language model is not * possible. Reasons for this can be: * @@ -196,8 +200,6 @@ declare module 'vscode' { * @see {@link LanguageModelChat.id} */ id?: string; - - // TODO@API tokens? min/max etc } /** @@ -210,11 +212,6 @@ declare module 'vscode' { */ export class LanguageModelError extends Error { - /** - * The language model does not exist. - */ - static NotFound(message?: string): LanguageModelError; - /** * The requestor does not have permissions to use this * language model @@ -226,6 +223,11 @@ declare module 'vscode' { */ static Blocked(message?: string): LanguageModelError; + /** + * The language model does not exist. + */ + static NotFound(message?: string): LanguageModelError; + /** * A code that identifies this error. * @@ -266,8 +268,11 @@ declare module 'vscode' { export const onDidChangeChatModels: Event; /** - * Select chat models by a {@link LanguageModelChatSelector selector}. This can yield in multiple or no chat models - * and extension must handle these cases, esp when no chat model exists. + * Select chat models by a {@link LanguageModelChatSelector selector}. This can yield in multiple or no chat models and + * extensions must handle these cases, esp. when no chat model exists, gracefully. + * + * *Note* that extensions can hold-on to the results returned by this function and use them later. However, whenever the + * {@link onDidChangeChatModels}-event is fired the list of chat models might have changed and extensions should re-query. * * @param selector A chat model selector. When omitted all chat models are returned. * @returns An array of chat models or `undefined` when no chat model was selected. From c6ca1ffaf9bde68a968f267a73c0016d48420b37 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 13 May 2024 10:50:12 +0200 Subject: [PATCH 116/357] voice - support synthesize pending chat responses (#212505) --- .../workbench/api/browser/mainThreadSpeech.ts | 14 +- .../browser/accessibilityConfiguration.ts | 11 +- src/vs/workbench/contrib/chat/browser/chat.ts | 5 +- .../contrib/chat/browser/chatQuick.ts | 2 +- .../contrib/chat/browser/chatWidget.ts | 16 +- .../contrib/chat/common/chatService.ts | 8 +- .../contrib/chat/common/chatServiceImpl.ts | 29 ++- .../contrib/chat/common/chatViewModel.ts | 5 + .../actions/voiceChatActions.ts | 224 ++++++++++++------ .../electron-sandbox/chat.contribution.ts | 4 +- .../browser/inlineChatController.ts | 14 +- .../inlineChat/browser/inlineChatWidget.ts | 6 +- .../contrib/speech/browser/speechService.ts | 161 +++++++------ .../contrib/speech/common/speechService.ts | 2 +- .../chat/browser/terminalChatController.ts | 23 +- 15 files changed, 336 insertions(+), 188 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadSpeech.ts b/src/vs/workbench/api/browser/mainThreadSpeech.ts index 189a77fce2a..6dbb9033772 100644 --- a/src/vs/workbench/api/browser/mainThreadSpeech.ts +++ b/src/vs/workbench/api/browser/mainThreadSpeech.ts @@ -3,11 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { raceCancellation } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { ILogService } from 'vs/platform/log/common/log'; import { ExtHostContext, ExtHostSpeechShape, MainContext, MainThreadSpeechShape } from 'vs/workbench/api/common/extHost.protocol'; -import { IKeywordRecognitionEvent, ISpeechProviderMetadata, ISpeechService, ISpeechToTextEvent, ITextToSpeechEvent } from 'vs/workbench/contrib/speech/common/speechService'; +import { IKeywordRecognitionEvent, ISpeechProviderMetadata, ISpeechService, ISpeechToTextEvent, ITextToSpeechEvent, TextToSpeechStatus } from 'vs/workbench/contrib/speech/common/speechService'; import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; type SpeechToTextSession = { @@ -16,7 +17,6 @@ type SpeechToTextSession = { type TextToSpeechSession = { readonly onDidChange: Emitter; - synthesize(text: string): Promise; }; type KeywordRecognitionSession = { @@ -86,10 +86,7 @@ export class MainThreadSpeech implements MainThreadSpeechShape { this.proxy.$createTextToSpeechSession(handle, session, options?.language); const onDidChange = disposables.add(new Emitter()); - this.textToSpeechSessions.set(session, { - onDidChange, - synthesize: text => this.proxy.$synthesizeSpeech(session, text) - }); + this.textToSpeechSessions.set(session, { onDidChange }); disposables.add(token.onCancellationRequested(() => { this.proxy.$cancelTextToSpeechSession(session); @@ -99,7 +96,10 @@ export class MainThreadSpeech implements MainThreadSpeechShape { return { onDidChange: onDidChange.event, - synthesize: text => this.proxy.$synthesizeSpeech(session, text) + synthesize: async text => { + await this.proxy.$synthesizeSpeech(session, text); + await raceCancellation(Event.toPromise(Event.filter(onDidChange.event, e => e.status === TextToSpeechStatus.Stopped)), token); + } }; }, createKeywordRecognitionSession: token => { diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts index dd0af503046..a9fef9c21f5 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts @@ -14,6 +14,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { Event } from 'vs/base/common/event'; import { isDefined } from 'vs/base/common/types'; +import { IProductService } from 'vs/platform/product/common/productService'; export const accessibilityHelpIsShown = new RawContextKey('accessibilityHelpIsShown', false, true); export const accessibleViewIsShown = new RawContextKey('accessibleViewIsShown', false, true); @@ -631,6 +632,7 @@ export function registerAccessibilityConfiguration() { export const enum AccessibilityVoiceSettingId { SpeechTimeout = 'accessibility.voice.speechTimeout', + AutoSynthesize = 'accessibility.voice.autoSynthesize', SpeechLanguage = SPEECH_LANGUAGE_CONFIG } export const SpeechTimeoutDefault = 1200; @@ -640,7 +642,8 @@ export class DynamicSpeechAccessibilityConfiguration extends Disposable implemen static readonly ID = 'workbench.contrib.dynamicSpeechAccessibilityConfiguration'; constructor( - @ISpeechService private readonly speechService: ISpeechService + @ISpeechService private readonly speechService: ISpeechService, + @IProductService private readonly productService: IProductService ) { super(); @@ -676,6 +679,12 @@ export class DynamicSpeechAccessibilityConfiguration extends Disposable implemen 'tags': ['accessibility'], 'enumDescriptions': languagesSorted.map(key => languages[key].name), 'enumItemLabels': languagesSorted.map(key => languages[key].name) + }, + [AccessibilityVoiceSettingId.AutoSynthesize]: { + 'type': 'boolean', + 'markdownDescription': localize('autoSynthesize', "Whether a textual response should automatically be read out aloud when speech was used as input. For example in a chat session, a response is automatically synthesized when voice was used as chat request."), + 'default': this.productService.quality !== 'stable', // TODO@bpasero decide on a default + 'tags': ['accessibility'] } } }); diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index 357ca063a54..d29591bc016 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -10,11 +10,13 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { Selection } from 'vs/editor/common/core/selection'; import { localize } from 'vs/nls'; import { MenuId } from 'vs/platform/actions/common/actions'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ChatViewPane } from 'vs/workbench/contrib/chat/browser/chatViewPane'; import { IChatWidgetContrib } from 'vs/workbench/contrib/chat/browser/chatWidget'; import { ICodeBlockActionContext } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; import { ChatAgentLocation, IChatAgentCommand, IChatAgentData } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatResponseModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { CHAT_PROVIDER_ID } from 'vs/workbench/contrib/chat/common/chatParticipantContribTypes'; import { IChatRequestViewModel, IChatResponseViewModel, IChatViewModel, IChatWelcomeMessageViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; @@ -135,6 +137,7 @@ export interface IChatWidget { readonly supportsFileReferences: boolean; readonly parsedInput: IParsedChatRequest; lastSelectedAgent: IChatAgentData | undefined; + readonly scopedContextKeyService: IContextKeyService; getContrib(id: string): T | undefined; reveal(item: ChatTreeItem): void; @@ -143,7 +146,7 @@ export interface IChatWidget { getFocus(): ChatTreeItem | undefined; setInput(query?: string): void; getInput(): string; - acceptInput(query?: string): void; + acceptInput(query?: string): Promise; acceptInputWithPrefix(prefix: string): void; setInputPlaceholder(placeholder: string): void; resetInputPlaceholder(): void; diff --git a/src/vs/workbench/contrib/chat/browser/chatQuick.ts b/src/vs/workbench/contrib/chat/browser/chatQuick.ts index d52ce247794..adc1a6eb791 100644 --- a/src/vs/workbench/contrib/chat/browser/chatQuick.ts +++ b/src/vs/workbench/contrib/chat/browser/chatQuick.ts @@ -271,7 +271,7 @@ class QuickChat extends Disposable { })); } - async acceptInput(): Promise { + async acceptInput() { return this.widget.acceptInput(); } diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 5a8b65c964e..cf784a5de5f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -33,7 +33,7 @@ import { ChatListDelegate, ChatListItemRenderer, IChatRendererDelegate } from 'v import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions'; import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { CONTEXT_CHAT_INPUT_HAS_AGENT, CONTEXT_CHAT_LOCATION, CONTEXT_CHAT_REQUEST_IN_PROGRESS, CONTEXT_IN_CHAT_SESSION, CONTEXT_RESPONSE_FILTERED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; -import { ChatModelInitState, IChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatModelInitState, IChatModel, IChatResponseModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { ChatRequestAgentPart, IParsedChatRequest, chatAgentLeader, chatSubcommandLeader, extractAgentAndCommand } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; import { IChatFollowup, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; @@ -161,6 +161,10 @@ export class ChatWidget extends Disposable implements IChatWidget { return this.parsedChatRequest; } + get scopedContextKeyService(): IContextKeyService { + return this.contextKeyService; + } + constructor( readonly location: ChatAgentLocation, readonly viewContext: IChatWidgetViewContext, @@ -689,8 +693,8 @@ export class ChatWidget extends Disposable implements IChatWidget { return this.inputPart.inputEditor.getValue(); } - async acceptInput(query?: string): Promise { - this._acceptInput(query ? { query } : undefined); + async acceptInput(query?: string): Promise { + return this._acceptInput(query ? { query } : undefined); } async acceptInputWithPrefix(prefix: string): Promise { @@ -707,7 +711,7 @@ export class ChatWidget extends Disposable implements IChatWidget { return inputState; } - private async _acceptInput(opts: { query: string } | { prefix: string } | undefined): Promise { + private async _acceptInput(opts: { query: string } | { prefix: string } | undefined): Promise { if (this.viewModel) { this._onDidAcceptInput.fire(); @@ -723,13 +727,15 @@ export class ChatWidget extends Disposable implements IChatWidget { const inputState = this.collectInputState(); this.inputPart.acceptInput(isUserQuery ? input : undefined, isUserQuery ? inputState : undefined); this._onDidSubmitAgent.fire({ agent: result.agent, slashCommand: result.slashCommand }); - result.responseCompletePromise.then(async () => { + result.responseCompletePromise.then(() => { const responses = this.viewModel?.getItems().filter(isResponseVM); const lastResponse = responses?.[responses.length - 1]; this.chatAccessibilityService.acceptResponse(lastResponse, requestId); }); + return result.responseCreatedPromise; } } + return undefined; } getCodeBlockInfosForResponse(response: IChatResponseViewModel): IChatCodeBlockInfo[] { diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 4f3b58978cd..9ffe4750693 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -14,7 +14,7 @@ import { Command, Location, TextEdit } from 'vs/editor/common/languages'; import { FileType } from 'vs/platform/files/common/files'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { ChatModel, IChatModel, IChatRequestModel, IChatRequestVariableData, IExportableChatData, ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatModel, IChatModel, IChatRequestModel, IChatRequestVariableData, IChatResponseModel, IExportableChatData, 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'; @@ -288,8 +288,12 @@ export interface IChatTransferredSessionData { inputValue: string; } -export interface IChatSendRequestData { +export interface IChatSendRequestResponseState { + responseCreatedPromise: Promise; responseCompletePromise: Promise; +} + +export interface IChatSendRequestData extends IChatSendRequestResponseState { agent: IChatAgentData; slashCommand?: IChatAgentCommand; } diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index 426e52d00f7..1efa8f9e2e3 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { coalesce } from 'vs/base/common/arrays'; +import { DeferredPromise } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { ErrorNoTelemetry } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; @@ -22,10 +23,10 @@ import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storag import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ChatAgentLocation, IChatAgent, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { ChatModel, ChatRequestModel, ChatWelcomeMessageModel, IChatModel, IChatRequestModel, IChatRequestVariableData, IChatRequestVariableEntry, IExportableChatData, ISerializableChatData, ISerializableChatsData, getHistoryEntriesFromModel, updateRanges } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatModel, ChatRequestModel, ChatWelcomeMessageModel, IChatModel, IChatRequestModel, IChatRequestVariableData, IChatRequestVariableEntry, IChatResponseModel, IExportableChatData, ISerializableChatData, ISerializableChatsData, getHistoryEntriesFromModel, updateRanges } from 'vs/workbench/contrib/chat/common/chatModel'; import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, IParsedChatRequest, chatAgentLeader, chatSubcommandLeader, getPromptText } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; -import { ChatCopyKind, IChatCompleteResponse, IChatDetail, IChatFollowup, IChatProgress, IChatSendRequestData, IChatSendRequestOptions, IChatService, IChatTransferredSessionData, IChatUserActionEvent, ChatAgentVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { ChatCopyKind, IChatCompleteResponse, IChatDetail, IChatFollowup, IChatProgress, IChatSendRequestData, IChatSendRequestOptions, IChatSendRequestResponseState, IChatService, IChatTransferredSessionData, IChatUserActionEvent, ChatAgentVoteDirection } 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 { ChatMessageRole, IChatMessage } from 'vs/workbench/contrib/chat/common/languageModels'; @@ -433,7 +434,7 @@ export class ChatService extends Disposable implements IChatService { this.removeRequest(model.sessionId, request.id); - await this._sendRequestAsync(model, model.sessionId, request.message, attempt, enableCommandDetection, implicitVariablesEnabled, defaultAgent, location); + await this._sendRequestAsync(model, model.sessionId, request.message, attempt, enableCommandDetection, implicitVariablesEnabled, defaultAgent, location).responseCompletePromise; } async sendRequest(sessionId: string, request: string, options?: IChatSendRequestOptions): Promise { @@ -467,7 +468,7 @@ export class ChatService extends Disposable implements IChatService { // This method is only returning whether the request was accepted - don't block on the actual request return { - responseCompletePromise: this._sendRequestAsync(model, sessionId, parsedRequest, attempt, !options?.noCommandDetection, implicitVariablesEnabled, defaultAgent, location, options), + ...this._sendRequestAsync(model, sessionId, parsedRequest, attempt, !options?.noCommandDetection, implicitVariablesEnabled, defaultAgent, location, options), agent, slashCommand: agentSlashCommandPart?.command, }; @@ -497,7 +498,7 @@ export class ChatService extends Disposable implements IChatService { return newTokenSource.token; } - private async _sendRequestAsync(model: ChatModel, sessionId: string, parsedRequest: IParsedChatRequest, attempt: number, enableCommandDetection: boolean, implicitVariablesEnabled: boolean, defaultAgent: IChatAgent, location: ChatAgentLocation, confirmData?: IRequestConfirmationData): Promise { + private _sendRequestAsync(model: ChatModel, sessionId: string, parsedRequest: IParsedChatRequest, attempt: number, enableCommandDetection: boolean, implicitVariablesEnabled: boolean, defaultAgent: IChatAgent, location: ChatAgentLocation, confirmData?: IRequestConfirmationData): IChatSendRequestResponseState { const followupsCancelToken = this.refreshFollowupsCancellationToken(sessionId); let request: ChatRequestModel; const agentPart = 'kind' in parsedRequest ? undefined : parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart); @@ -507,6 +508,15 @@ export class ChatService extends Disposable implements IChatService { let gotProgress = false; const requestType = commandPart ? 'slashCommand' : 'string'; + const responseCreated = new DeferredPromise(); + let responseCreatedComplete = false; + function completeResponseCreated(): void { + if (!responseCreatedComplete && request?.response) { + responseCreated.complete(request.response); + responseCreatedComplete = true; + } + } + const source = new CancellationTokenSource(); const token = source.token; const sendRequestInternal = async () => { @@ -524,6 +534,7 @@ export class ChatService extends Disposable implements IChatService { } model.acceptResponseProgress(request, progress); + completeResponseCreated(); }; const stopWatch = new StopWatch(false); @@ -554,6 +565,7 @@ export class ChatService extends Disposable implements IChatService { const initVariableData: IChatRequestVariableData = { variables: [] }; request = model.addRequest(parsedRequest, initVariableData, attempt, agent, agentSlashCommandPart?.command); + completeResponseCreated(); const variableData = await this.chatVariablesService.resolveVariables(parsedRequest, model, progressCallback, token); request.variableData = variableData; @@ -589,6 +601,7 @@ export class ChatService extends Disposable implements IChatService { agentOrCommandFollowups = this.chatAgentService.getFollowups(agent.id, requestProps, agentResult, history, followupsCancelToken); } else if (commandPart && this.chatSlashCommandService.hasCommand(commandPart.slashCommand.command)) { request = model.addRequest(parsedRequest, { variables: [] }, attempt); + completeResponseCreated(); // contributed slash commands // TODO: spell this out in the UI const history: IChatMessage[] = []; @@ -632,6 +645,7 @@ export class ChatService extends Disposable implements IChatService { chatSessionId: model.sessionId }); model.setResponse(request, rawResult); + completeResponseCreated(); this.trace('sendRequest', `Provider returned response for session ${model.sessionId}`); model.completeResponse(request); @@ -650,7 +664,10 @@ export class ChatService extends Disposable implements IChatService { rawResponsePromise.finally(() => { this._pendingRequests.deleteAndDispose(model.sessionId); }); - return rawResponsePromise; + return { + responseCreatedPromise: responseCreated.p, + responseCompletePromise: rawResponsePromise, + }; } async removeRequest(sessionId: string, requestId: string): Promise { diff --git a/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/chatViewModel.ts index ee9204379d2..227fb9439e6 100644 --- a/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatViewModel.ts @@ -114,6 +114,7 @@ export interface IChatLiveUpdateData { } export interface IChatResponseViewModel { + readonly model: IChatResponseModel; readonly id: string; readonly sessionId: string; /** This ID updates every time the underlying data changes */ @@ -362,6 +363,10 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi private readonly _onDidChange = this._register(new Emitter()); readonly onDidChange = this._onDidChange.event; + get model() { + return this._model; + } + get id() { return this._model.id; } diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts index 927f83dbd43..3a1eede292f 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/voiceChatActions'; -import { RunOnceScheduler, disposableTimeout } from 'vs/base/common/async'; -import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { RunOnceScheduler, disposableTimeout, raceCancellation } from 'vs/base/common/async'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { Codicon } from 'vs/base/common/codicons'; import { Color } from 'vs/base/common/color'; import { Event } from 'vs/base/common/event'; @@ -34,7 +34,7 @@ import { ActiveEditorContext } from 'vs/workbench/common/contextkeys'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { ACTIVITY_BAR_BADGE_BACKGROUND } from 'vs/workbench/common/theme'; import { AccessibilityVoiceSettingId, SpeechTimeoutDefault, accessibilityConfigurationNodeBase } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { CHAT_CATEGORY, stringifyItem } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; +import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; import { IChatExecuteActionContext } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions'; import { CHAT_VIEW_ID, IChatWidget, IChatWidgetService, IQuickChatService, showChatView } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatAgentLocation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; @@ -46,7 +46,7 @@ import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/com import { InlineChatController } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; -import { HasSpeechProvider, ISpeechService, KeywordRecognitionStatus, SpeechToTextInProgress, SpeechToTextStatus, TextToSpeechInProgress } from 'vs/workbench/contrib/speech/common/speechService'; +import { HasSpeechProvider, ISpeechService, ITextToSpeechSession, KeywordRecognitionStatus, SpeechToTextInProgress, SpeechToTextStatus, TextToSpeechInProgress } from 'vs/workbench/contrib/speech/common/speechService'; import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalChatContextKeys, TerminalChatController } from 'vs/workbench/contrib/terminal/browser/terminalContribExports'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -54,6 +54,8 @@ import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/statusbar/browser/statusbar'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; +import { IChatResponseModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; //#region Speech to Text @@ -62,7 +64,7 @@ const CONTEXT_VOICE_CHAT_IN_PROGRESS = new RawContextKey('voiceChatInPr const CONTEXT_QUICK_VOICE_CHAT_IN_PROGRESS = new RawContextKey('quickVoiceChatInProgress', false, { type: 'boolean', description: localize('quickVoiceChatInProgress', "True when voice recording from microphone is in progress for quick chat.") }); const CONTEXT_INLINE_VOICE_CHAT_IN_PROGRESS = new RawContextKey('inlineVoiceChatInProgress', false, { type: 'boolean', description: localize('inlineVoiceChatInProgress', "True when voice recording from microphone is in progress for inline chat.") }); -const CONTEXT_TERMINAL_VOICE_CHAT_IN_PROGRESS = new RawContextKey('terminalVoiceChatInProgress', false, { type: 'boolean', description: localize('terminalVoiceChatInProgress', "True when voice recording from microphone is in progress for terminal chat.") }); +const CONTEXT_VOICE_CHAT_IN_TERMINAL_IN_PROGRESS = new RawContextKey('voiceChatInTerminalInProgress', false, { type: 'boolean', description: localize('voiceChatInTerminalInProgress', "True when voice recording from microphone is in progress for terminal chat.") }); const CONTEXT_VOICE_CHAT_IN_VIEW_IN_PROGRESS = new RawContextKey('voiceChatInViewInProgress', false, { type: 'boolean', description: localize('voiceChatInViewInProgress', "True when voice recording from microphone is in progress in the chat view.") }); const CONTEXT_VOICE_CHAT_IN_EDITOR_IN_PROGRESS = new RawContextKey('voiceChatInEditorInProgress', false, { type: 'boolean', description: localize('voiceChatInEditorInProgress', "True when voice recording from microphone is in progress in the chat editor.") }); @@ -73,6 +75,12 @@ const AnyChatRequestInProgress = ContextKeyExpr.or(CONTEXT_CHAT_REQUEST_IN_PROGR type VoiceChatSessionContext = 'inline' | 'terminal' | 'quick' | 'view' | 'editor'; +enum VoiceChatSessionState { + Stopped = 1, + GettingReady, + Started +} + interface IVoiceChatSessionController { readonly onDidAcceptInput: Event; @@ -80,8 +88,10 @@ interface IVoiceChatSessionController { readonly context: VoiceChatSessionContext; + updateState(state: VoiceChatSessionState): void; + focusInput(): void; - acceptInput(): void; + acceptInput(): Promise; updateInput(text: string): void; getInput(): string; @@ -210,6 +220,32 @@ class VoiceChatSessionControllerFactory { return VoiceChatSessionControllerFactory.doCreateForChatViewOrEditor('editor', chatView, viewsService); } + private static createContextKeyController(contextKeyService: IContextKeyService, rawControllerVoiceChatInProgress: RawContextKey): (state: VoiceChatSessionState) => void { + const contextVoiceChatGettingReady = CONTEXT_VOICE_CHAT_GETTING_READY.bindTo(contextKeyService); + const contextVoiceChatInProgress = CONTEXT_VOICE_CHAT_IN_PROGRESS.bindTo(contextKeyService); + const controllerVoiceChatInProgress = rawControllerVoiceChatInProgress.bindTo(contextKeyService); + + return (state: VoiceChatSessionState) => { + switch (state) { + case VoiceChatSessionState.GettingReady: + contextVoiceChatGettingReady.set(true); + contextVoiceChatInProgress.set(false); + controllerVoiceChatInProgress.set(false); + break; + case VoiceChatSessionState.Started: + contextVoiceChatGettingReady.set(false); + contextVoiceChatInProgress.set(true); + controllerVoiceChatInProgress.set(true); + break; + case VoiceChatSessionState.Stopped: + contextVoiceChatGettingReady.set(false); + contextVoiceChatInProgress.set(false); + controllerVoiceChatInProgress.set(false); + break; + } + }; + } + private static doCreateForChatViewOrEditor(context: 'view' | 'editor', chatView: IChatWidget, viewsService: IViewsService): IVoiceChatSessionController { return { context, @@ -221,7 +257,8 @@ class VoiceChatSessionControllerFactory { updateInput: text => chatView.setInput(text), getInput: () => chatView.getInput(), setInputPlaceholder: text => chatView.setInputPlaceholder(text), - clearInputPlaceholder: () => chatView.resetInputPlaceholder() + clearInputPlaceholder: () => chatView.resetInputPlaceholder(), + updateState: VoiceChatSessionControllerFactory.createContextKeyController(chatView.scopedContextKeyService, CONTEXT_VOICE_CHAT_IN_VIEW_IN_PROGRESS) }; } @@ -235,7 +272,8 @@ class VoiceChatSessionControllerFactory { updateInput: text => quickChat.setInput(text), getInput: () => quickChat.getInput(), setInputPlaceholder: text => quickChat.setInputPlaceholder(text), - clearInputPlaceholder: () => quickChat.resetInputPlaceholder() + clearInputPlaceholder: () => quickChat.resetInputPlaceholder(), + updateState: VoiceChatSessionControllerFactory.createContextKeyController(quickChat.scopedContextKeyService, CONTEXT_QUICK_VOICE_CHAT_IN_PROGRESS) }; } @@ -254,7 +292,8 @@ class VoiceChatSessionControllerFactory { updateInput: text => inlineChat.updateInput(text, false), getInput: () => inlineChat.getInput(), setInputPlaceholder: text => inlineChat.setPlaceholder(text), - clearInputPlaceholder: () => inlineChat.resetPlaceholder() + clearInputPlaceholder: () => inlineChat.resetPlaceholder(), + updateState: VoiceChatSessionControllerFactory.createContextKeyController(inlineChat.scopedContextKeyService, CONTEXT_INLINE_VOICE_CHAT_IN_PROGRESS) }; } @@ -268,7 +307,8 @@ class VoiceChatSessionControllerFactory { updateInput: text => terminalChat.updateInput(text, false), getInput: () => terminalChat.getInput(), setInputPlaceholder: text => terminalChat.setPlaceholder(text), - clearInputPlaceholder: () => terminalChat.resetPlaceholder() + clearInputPlaceholder: () => terminalChat.resetPlaceholder(), + updateState: VoiceChatSessionControllerFactory.createContextKeyController(terminalChat.scopedContextKeyService, CONTEXT_VOICE_CHAT_IN_TERMINAL_IN_PROGRESS) }; } } @@ -297,23 +337,14 @@ class VoiceChatSessions { return VoiceChatSessions.instance; } - private voiceChatInProgressKey = CONTEXT_VOICE_CHAT_IN_PROGRESS.bindTo(this.contextKeyService); - private voiceChatGettingReadyKey = CONTEXT_VOICE_CHAT_GETTING_READY.bindTo(this.contextKeyService); - - private quickVoiceChatInProgressKey = CONTEXT_QUICK_VOICE_CHAT_IN_PROGRESS.bindTo(this.contextKeyService); - private inlineVoiceChatInProgressKey = CONTEXT_INLINE_VOICE_CHAT_IN_PROGRESS.bindTo(this.contextKeyService); - private terminalVoiceChatInProgressKey = CONTEXT_TERMINAL_VOICE_CHAT_IN_PROGRESS.bindTo(this.contextKeyService); - private voiceChatInViewInProgressKey = CONTEXT_VOICE_CHAT_IN_VIEW_IN_PROGRESS.bindTo(this.contextKeyService); - private voiceChatInEditorInProgressKey = CONTEXT_VOICE_CHAT_IN_EDITOR_IN_PROGRESS.bindTo(this.contextKeyService); - private currentVoiceChatSession: IActiveVoiceChatSession | undefined = undefined; private voiceChatSessionIds = 0; constructor( - @IContextKeyService private readonly contextKeyService: IContextKeyService, @IVoiceChatService private readonly voiceChatService: IVoiceChatService, @IConfigurationService private readonly configurationService: IConfigurationService, - @IInstantiationService private readonly instantiationService: IInstantiationService + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IAccessibilityService private readonly accessibilityService: IAccessibilityService ) { } async start(controller: IVoiceChatSessionController, context?: IChatExecuteActionContext): Promise { @@ -330,7 +361,7 @@ class VoiceChatSessions { controller, disposables: new DisposableStore(), setTimeoutDisabled: (disabled: boolean) => { disableTimeout = disabled; }, - accept: () => session.controller.acceptInput(), + accept: () => this.accept(sessionId), stop: () => this.stop(sessionId, controller.context) }; @@ -342,7 +373,7 @@ class VoiceChatSessions { controller.focusInput(); - this.voiceChatGettingReadyKey.set(true); + controller.updateState(VoiceChatSessionState.GettingReady); const voiceChatSession = await this.voiceChatService.createVoiceChatSession(cts.token, { usesAgents: controller.context !== 'inline', model: context?.widget?.viewModel?.model }); @@ -353,7 +384,7 @@ class VoiceChatSessions { voiceChatTimeout = SpeechTimeoutDefault; } - const acceptTranscriptionScheduler = session.disposables.add(new RunOnceScheduler(() => session.controller.acceptInput(), voiceChatTimeout)); + const acceptTranscriptionScheduler = session.disposables.add(new RunOnceScheduler(() => this.accept(sessionId), voiceChatTimeout)); session.disposables.add(voiceChatSession.onDidChange(({ status, text, waitingForInput }) => { if (cts.token.isCancellationRequested) { return; @@ -390,26 +421,7 @@ class VoiceChatSessions { } private onDidSpeechToTextSessionStart(controller: IVoiceChatSessionController, disposables: DisposableStore): void { - this.voiceChatGettingReadyKey.set(false); - this.voiceChatInProgressKey.set(true); - - switch (controller.context) { - case 'inline': - this.inlineVoiceChatInProgressKey.set(true); - break; - case 'terminal': - this.terminalVoiceChatInProgressKey.set(true); - break; - case 'quick': - this.quickVoiceChatInProgressKey.set(true); - break; - case 'view': - this.voiceChatInViewInProgressKey.set(true); - break; - case 'editor': - this.voiceChatInEditorInProgressKey.set(true); - break; - } + controller.updateState(VoiceChatSessionState.Started); let dotCount = 0; @@ -434,20 +446,13 @@ class VoiceChatSessions { this.currentVoiceChatSession.controller.clearInputPlaceholder(); + this.currentVoiceChatSession.controller.updateState(VoiceChatSessionState.Stopped); + this.currentVoiceChatSession.disposables.dispose(); this.currentVoiceChatSession = undefined; - - this.voiceChatGettingReadyKey.set(false); - this.voiceChatInProgressKey.set(false); - - this.quickVoiceChatInProgressKey.set(false); - this.inlineVoiceChatInProgressKey.set(false); - this.terminalVoiceChatInProgressKey.set(false); - this.voiceChatInViewInProgressKey.set(false); - this.voiceChatInEditorInProgressKey.set(false); } - accept(voiceChatSessionId = this.voiceChatSessionIds): void { + async accept(voiceChatSessionId = this.voiceChatSessionIds): Promise { if ( !this.currentVoiceChatSession || this.voiceChatSessionIds !== voiceChatSessionId @@ -455,7 +460,17 @@ class VoiceChatSessions { return; } - this.currentVoiceChatSession.controller.acceptInput(); + const response = await this.currentVoiceChatSession.controller.acceptInput(); + if (!response) { + return; + } + + if ( + !this.accessibilityService.isScreenReaderOptimized() && // do not synthesize when screen reader is active + this.configurationService.getValue(AccessibilityVoiceSettingId.AutoSynthesize) === true + ) { + ChatSynthesizerSessions.getInstance(this.instantiationService).start(response); + } } } @@ -647,8 +662,8 @@ export class StartVoiceChatAction extends Action2 { id: MenuId.for('terminalChatInput'), when: ContextKeyExpr.and( HasSpeechProvider, - TextToSpeechInProgress.negate(), // hide when text to speech is in progress - CONTEXT_TERMINAL_VOICE_CHAT_IN_PROGRESS.negate(), // hide when voice chat is in progress + TextToSpeechInProgress.negate(), // hide when text to speech is in progress + CONTEXT_VOICE_CHAT_IN_TERMINAL_IN_PROGRESS.negate(), // hide when voice chat is in progress ), group: 'navigation', order: -1 @@ -729,7 +744,6 @@ class BaseStopListeningAction extends Action2 { constructor( desc: { id: string; icon?: ThemeIcon; f1?: boolean }, - private readonly target: 'inline' | 'terminal' | 'quick' | 'view' | 'editor' | undefined, context: RawContextKey, menu: MenuId | undefined, ) { @@ -751,8 +765,8 @@ class BaseStopListeningAction extends Action2 { }); } - async run(accessor: ServicesAccessor, context?: IChatExecuteActionContext): Promise { - VoiceChatSessions.getInstance(accessor.get(IInstantiationService)).stop(undefined, this.target); + async run(accessor: ServicesAccessor): Promise { + VoiceChatSessions.getInstance(accessor.get(IInstantiationService)).stop(); } } @@ -761,7 +775,7 @@ export class StopListeningAction extends BaseStopListeningAction { static readonly ID = 'workbench.action.chat.stopListening'; constructor() { - super({ id: StopListeningAction.ID, f1: true }, undefined, CONTEXT_VOICE_CHAT_IN_PROGRESS, undefined); + super({ id: StopListeningAction.ID, f1: true }, CONTEXT_VOICE_CHAT_IN_PROGRESS, undefined); } } @@ -770,7 +784,7 @@ export class StopListeningInChatViewAction extends BaseStopListeningAction { static readonly ID = 'workbench.action.chat.stopListeningInChatView'; constructor() { - super({ id: StopListeningInChatViewAction.ID, icon: spinningLoading }, 'view', CONTEXT_VOICE_CHAT_IN_VIEW_IN_PROGRESS, MenuId.ChatExecute); + super({ id: StopListeningInChatViewAction.ID, icon: spinningLoading }, CONTEXT_VOICE_CHAT_IN_VIEW_IN_PROGRESS, MenuId.ChatExecute); } } @@ -779,7 +793,7 @@ export class StopListeningInChatEditorAction extends BaseStopListeningAction { static readonly ID = 'workbench.action.chat.stopListeningInChatEditor'; constructor() { - super({ id: StopListeningInChatEditorAction.ID, icon: spinningLoading }, 'editor', CONTEXT_VOICE_CHAT_IN_EDITOR_IN_PROGRESS, MenuId.ChatExecute); + super({ id: StopListeningInChatEditorAction.ID, icon: spinningLoading }, CONTEXT_VOICE_CHAT_IN_EDITOR_IN_PROGRESS, MenuId.ChatExecute); } } @@ -788,7 +802,7 @@ export class StopListeningInQuickChatAction extends BaseStopListeningAction { static readonly ID = 'workbench.action.chat.stopListeningInQuickChat'; constructor() { - super({ id: StopListeningInQuickChatAction.ID, icon: spinningLoading }, 'quick', CONTEXT_QUICK_VOICE_CHAT_IN_PROGRESS, MenuId.ChatExecute); + super({ id: StopListeningInQuickChatAction.ID, icon: spinningLoading }, CONTEXT_QUICK_VOICE_CHAT_IN_PROGRESS, MenuId.ChatExecute); } } @@ -797,7 +811,7 @@ export class StopListeningInTerminalChatAction extends BaseStopListeningAction { static readonly ID = 'workbench.action.chat.stopListeningInTerminalChat'; constructor() { - super({ id: StopListeningInTerminalChatAction.ID, icon: spinningLoading }, 'terminal', CONTEXT_TERMINAL_VOICE_CHAT_IN_PROGRESS, MenuId.for('terminalChatInput')); + super({ id: StopListeningInTerminalChatAction.ID, icon: spinningLoading }, CONTEXT_VOICE_CHAT_IN_TERMINAL_IN_PROGRESS, MenuId.for('terminalChatInput')); } } @@ -847,7 +861,7 @@ class ChatSynthesizerSessions { @IInstantiationService private readonly instantiationService: IInstantiationService ) { } - async start(text: string): Promise { + async start(response: IChatResponseModel): Promise { // Stop running text-to-speech or speech-to-text sessions in chats this.stop(); @@ -856,7 +870,69 @@ class ChatSynthesizerSessions { const activeSession = this.activeSession = new CancellationTokenSource(); const session = await this.speechService.createTextToSpeechSession(activeSession.token, 'chat'); - session.synthesize(text); + + if (activeSession.token.isCancellationRequested) { + return; + } + + if (response.isComplete) { + return this.synthesizeCompletedResponse(session, response); + } else { + return this.synthesizePendingResponse(session, response, activeSession.token); + } + } + + private synthesizeCompletedResponse(session: ITextToSpeechSession, response: IChatResponseModel): Promise { + return session.synthesize(response.response.asString()); + } + + private async synthesizePendingResponse(session: ITextToSpeechSession, response: IChatResponseModel, token: CancellationToken): Promise { + for await (const chunk of this.nextChatResponseChunk(response, token)) { + if (token.isCancellationRequested) { + return; + } + + await raceCancellation(session.synthesize(chunk), token); + } + } + + private async *nextChatResponseChunk(response: IChatResponseModel, token: CancellationToken): AsyncIterable { + let totalOffset = 0; + let complete = false; + do { + const text = response.response.asString(); + const { chunks, offset, tail } = this.toChunks(text, totalOffset); + totalOffset = offset; + complete = response.isComplete; + + for (const chunk of chunks) { + yield chunk; + + if (token.isCancellationRequested) { + return; + } + } + + if (complete) { + yield tail; + } else if (text === response.response.asString()) { + await raceCancellation(Event.toPromise(response.onDidChange), token); // wait for the response to change + } + } while (!token.isCancellationRequested && !complete); + } + + private toChunks(text: string, offset: number): { readonly chunks: string[]; readonly offset: number; readonly tail: string } { + const chunks: string[] = []; + + for (let i = offset; i < text.length; i++) { + const char = text[i]; + if (char === '.' || char === '!' || char === '?' || char === ':') { + chunks.push(text.substring(offset, i + 1)); + offset = i + 1; + } + } + + return { chunks, offset, tail: text.substring(offset) }; } stop(): void { @@ -888,11 +964,11 @@ export class InstallSpeechProviderForSynthesizeChatAction extends BaseInstallSpe } } -export class ReadChatItemAloud extends Action2 { +export class ReadChatResponseAloud extends Action2 { constructor() { super({ - id: 'workbench.action.chat.readChatItemAloud', - title: localize2('workbench.action.chat.readChatItemAloud', "Read Aloud"), + id: 'workbench.action.chat.readChatResponseAloud', + title: localize2('workbench.action.chat.readChatResponseAloud', "Read Aloud"), f1: false, icon: Codicon.unmute, precondition: CanVoiceChat, @@ -910,12 +986,12 @@ export class ReadChatItemAloud extends Action2 { } run(accessor: ServicesAccessor, ...args: any[]) { - const item = args[0]; - if (!isResponseVM(item)) { + const response = args[0]; + if (!isResponseVM(response)) { return; } - ChatSynthesizerSessions.getInstance(accessor.get(IInstantiationService)).start(stringifyItem(item, false)); + ChatSynthesizerSessions.getInstance(accessor.get(IInstantiationService)).start(response.model); } } @@ -952,7 +1028,7 @@ export class StopReadAloud extends Action2 { }); } - async run(accessor: ServicesAccessor, ...args: any[]) { + async run(accessor: ServicesAccessor) { ChatSynthesizerSessions.getInstance(accessor.get(IInstantiationService)).stop(); } } diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts b/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts index 682f22897cd..443aafd4150 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { InlineVoiceChatAction, QuickVoiceChatAction, StartVoiceChatAction, StopListeningInQuickChatAction, StopListeningInChatEditorAction, StopListeningInChatViewAction, VoiceChatInChatViewAction, StopListeningAction, StopListeningAndSubmitAction, KeywordActivationContribution, InstallSpeechProviderForSynthesizeChatAction, InstallSpeechProviderForVoiceChatAction, StopListeningInTerminalChatAction, HoldToVoiceChatInChatViewAction, ReadChatItemAloud, StopReadAloud, StopReadChatItemAloud } from 'vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions'; +import { InlineVoiceChatAction, QuickVoiceChatAction, StartVoiceChatAction, StopListeningInQuickChatAction, StopListeningInChatEditorAction, StopListeningInChatViewAction, VoiceChatInChatViewAction, StopListeningAction, StopListeningAndSubmitAction, KeywordActivationContribution, InstallSpeechProviderForSynthesizeChatAction, InstallSpeechProviderForVoiceChatAction, StopListeningInTerminalChatAction, HoldToVoiceChatInChatViewAction, ReadChatResponseAloud, StopReadAloud, StopReadChatItemAloud } from 'vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions'; import { registerAction2 } from 'vs/platform/actions/common/actions'; import { WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; @@ -23,7 +23,7 @@ registerAction2(StopListeningInChatEditorAction); registerAction2(StopListeningInQuickChatAction); registerAction2(StopListeningInTerminalChatAction); -registerAction2(ReadChatItemAloud); +registerAction2(ReadChatResponseAloud); registerAction2(StopReadChatItemAloud); registerAction2(StopReadAloud); registerAction2(InstallSpeechProviderForSynthesizeChatAction); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 121ee198eb4..1eb3cd093c1 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -1019,6 +1019,14 @@ export class InlineChatController implements IEditorContribution { // ---- controller API + get scopedContextKeyService(): IContextKeyService { + if (this._input.value.isVisible) { + return this._input.value.chatWidget.scopedContextKeyService; + } else { + return this._zone.value.widget.chatWidget.scopedContextKeyService; + } + } + showSaveHint(): void { const status = localize('savehint', "Accept or discard changes to continue saving"); this._zone.value.widget.updateStatus(status, { classes: ['warn'] }); @@ -1034,11 +1042,11 @@ export class InlineChatController implements IEditorContribution { this._updatePlaceholder(); } - acceptInput(): void { + acceptInput() { if (this._input.value.isVisible) { - this._input.value.chatWidget.acceptInput(); + return this._input.value.chatWidget.acceptInput(); } else { - this._zone.value.widget.chatWidget.acceptInput(); + return this._zone.value.widget.chatWidget.acceptInput(); } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index 518a5c19f74..aa8c32c6391 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -131,6 +131,8 @@ export class InlineChatWidget { private _isLayouting: boolean = false; + readonly scopedContextKeyService: IContextKeyService; + private readonly _followUpDisposables = this._store.add(new DisposableStore()); constructor( location: ChatAgentLocation, @@ -151,11 +153,11 @@ export class InlineChatWidget { let allowRequests = false; - + this.scopedContextKeyService = this._store.add(_contextKeyService.createScoped(this._elements.chatWidget)); const scopedInstaService = _instantiationService.createChild( new ServiceCollection([ IContextKeyService, - this._store.add(_contextKeyService.createScoped(this._elements.chatWidget)) + this.scopedContextKeyService ]) ); diff --git a/src/vs/workbench/contrib/speech/browser/speechService.ts b/src/vs/workbench/contrib/speech/browser/speechService.ts index 7361470ddc8..b0efa674be4 100644 --- a/src/vs/workbench/contrib/speech/browser/speechService.ts +++ b/src/vs/workbench/contrib/speech/browser/speechService.ts @@ -12,7 +12,7 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ILogService } from 'vs/platform/log/common/log'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { DeferredPromise } from 'vs/base/common/async'; -import { ISpeechService, ISpeechProvider, HasSpeechProvider, ISpeechToTextSession, SpeechToTextInProgress, IKeywordRecognitionSession, KeywordRecognitionStatus, SpeechToTextStatus, speechLanguageConfigToLanguage, SPEECH_LANGUAGE_CONFIG, ITextToSpeechSession, TextToSpeechInProgress, TextToSpeechStatus } from 'vs/workbench/contrib/speech/common/speechService'; +import { ISpeechService, ISpeechProvider, HasSpeechProvider, ISpeechToTextSession, SpeechToTextInProgress, KeywordRecognitionStatus, SpeechToTextStatus, speechLanguageConfigToLanguage, SPEECH_LANGUAGE_CONFIG, ITextToSpeechSession, TextToSpeechInProgress, TextToSpeechStatus } from 'vs/workbench/contrib/speech/common/speechService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; @@ -134,8 +134,8 @@ export class SpeechService extends Disposable implements ISpeechService { private readonly _onDidEndSpeechToTextSession = this._register(new Emitter()); readonly onDidEndSpeechToTextSession = this._onDidEndSpeechToTextSession.event; - private _activeSpeechToTextSession: ISpeechToTextSession | undefined = undefined; - get hasActiveSpeechToTextSession() { return !!this._activeSpeechToTextSession; } + private activeSpeechToTextSessions = 0; + get hasActiveSpeechToTextSession() { return this.activeSpeechToTextSessions > 0; } private readonly speechToTextInProgress = SpeechToTextInProgress.bindTo(this.contextKeyService); @@ -143,7 +143,7 @@ export class SpeechService extends Disposable implements ISpeechService { const provider = await this.getProvider(); const language = speechLanguageConfigToLanguage(this.configurationService.getValue(SPEECH_LANGUAGE_CONFIG)); - const session = this._activeSpeechToTextSession = provider.createSpeechToTextSession(token, typeof language === 'string' ? { language } : undefined); + const session = provider.createSpeechToTextSession(token, typeof language === 'string' ? { language } : undefined); const sessionStart = Date.now(); let sessionRecognized = false; @@ -153,38 +153,38 @@ export class SpeechService extends Disposable implements ISpeechService { const disposables = new DisposableStore(); const onSessionStoppedOrCanceled = () => { - if (session === this._activeSpeechToTextSession) { - this._activeSpeechToTextSession = undefined; + this.activeSpeechToTextSessions--; + if (!this.hasActiveSpeechToTextSession) { this.speechToTextInProgress.reset(); - this._onDidEndSpeechToTextSession.fire(); - - type SpeechToTextSessionClassification = { - owner: 'bpasero'; - comment: 'An event that fires when a speech to text session is created'; - context: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Context of the session.' }; - sessionDuration: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Duration of the session.' }; - sessionRecognized: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'If speech was recognized.' }; - sessionError: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'If speech resulted in error.' }; - sessionContentLength: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Length of the recognized text.' }; - sessionLanguage: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Configured language for the session.' }; - }; - type SpeechToTextSessionEvent = { - context: string; - sessionDuration: number; - sessionRecognized: boolean; - sessionError: boolean; - sessionContentLength: number; - sessionLanguage: string; - }; - this.telemetryService.publicLog2('speechToTextSession', { - context, - sessionDuration: Date.now() - sessionStart, - sessionRecognized, - sessionError, - sessionContentLength, - sessionLanguage: language - }); } + this._onDidEndSpeechToTextSession.fire(); + + type SpeechToTextSessionClassification = { + owner: 'bpasero'; + comment: 'An event that fires when a speech to text session is created'; + context: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Context of the session.' }; + sessionDuration: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Duration of the session.' }; + sessionRecognized: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'If speech was recognized.' }; + sessionError: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'If speech resulted in error.' }; + sessionContentLength: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Length of the recognized text.' }; + sessionLanguage: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Configured language for the session.' }; + }; + type SpeechToTextSessionEvent = { + context: string; + sessionDuration: number; + sessionRecognized: boolean; + sessionError: boolean; + sessionContentLength: number; + sessionLanguage: string; + }; + this.telemetryService.publicLog2('speechToTextSession', { + context, + sessionDuration: Date.now() - sessionStart, + sessionRecognized, + sessionError, + sessionContentLength, + sessionLanguage: language + }); disposables.dispose(); }; @@ -197,10 +197,9 @@ export class SpeechService extends Disposable implements ISpeechService { disposables.add(session.onDidChange(e => { switch (e.status) { case SpeechToTextStatus.Started: - if (session === this._activeSpeechToTextSession) { - this.speechToTextInProgress.set(true); - this._onDidStartSpeechToTextSession.fire(); - } + this.activeSpeechToTextSessions++; + this.speechToTextInProgress.set(true); + this._onDidStartSpeechToTextSession.fire(); break; case SpeechToTextStatus.Recognizing: sessionRecognized = true; @@ -248,8 +247,8 @@ export class SpeechService extends Disposable implements ISpeechService { private readonly _onDidEndTextToSpeechSession = this._register(new Emitter()); readonly onDidEndTextToSpeechSession = this._onDidEndTextToSpeechSession.event; - private _activeTextToSpeechSession: ITextToSpeechSession | undefined = undefined; - get hasActiveTextToSpeechSession() { return !!this._activeTextToSpeechSession; } + private activeTextToSpeechSessions = 0; + get hasActiveTextToSpeechSession() { return this.activeTextToSpeechSessions > 0; } private readonly textToSpeechInProgress = TextToSpeechInProgress.bindTo(this.contextKeyService); @@ -257,59 +256,60 @@ export class SpeechService extends Disposable implements ISpeechService { const provider = await this.getProvider(); const language = speechLanguageConfigToLanguage(this.configurationService.getValue(SPEECH_LANGUAGE_CONFIG)); - const session = this._activeTextToSpeechSession = provider.createTextToSpeechSession(token, typeof language === 'string' ? { language } : undefined); + const session = provider.createTextToSpeechSession(token, typeof language === 'string' ? { language } : undefined); const sessionStart = Date.now(); let sessionError = false; const disposables = new DisposableStore(); - const onSessionStoppedOrCanceled = () => { - if (session === this._activeTextToSpeechSession) { - this._activeTextToSpeechSession = undefined; + const onSessionStoppedOrCanceled = (dispose: boolean) => { + this.activeTextToSpeechSessions--; + if (!this.hasActiveTextToSpeechSession) { this.textToSpeechInProgress.reset(); - this._onDidEndTextToSpeechSession.fire(); - - type TextToSpeechSessionClassification = { - owner: 'bpasero'; - comment: 'An event that fires when a text to speech session is created'; - context: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Context of the session.' }; - sessionDuration: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Duration of the session.' }; - sessionError: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'If speech resulted in error.' }; - sessionLanguage: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Configured language for the session.' }; - }; - type TextToSpeechSessionEvent = { - context: string; - sessionDuration: number; - sessionError: boolean; - sessionLanguage: string; - }; - this.telemetryService.publicLog2('textToSpeechSession', { - context, - sessionDuration: Date.now() - sessionStart, - sessionError, - sessionLanguage: language - }); } + this._onDidEndTextToSpeechSession.fire(); - disposables.dispose(); + type TextToSpeechSessionClassification = { + owner: 'bpasero'; + comment: 'An event that fires when a text to speech session is created'; + context: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Context of the session.' }; + sessionDuration: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Duration of the session.' }; + sessionError: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'If speech resulted in error.' }; + sessionLanguage: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Configured language for the session.' }; + }; + type TextToSpeechSessionEvent = { + context: string; + sessionDuration: number; + sessionError: boolean; + sessionLanguage: string; + }; + this.telemetryService.publicLog2('textToSpeechSession', { + context, + sessionDuration: Date.now() - sessionStart, + sessionError, + sessionLanguage: language + }); + + if (dispose) { + disposables.dispose(); + } }; - disposables.add(token.onCancellationRequested(() => onSessionStoppedOrCanceled())); + disposables.add(token.onCancellationRequested(() => onSessionStoppedOrCanceled(true))); if (token.isCancellationRequested) { - onSessionStoppedOrCanceled(); + onSessionStoppedOrCanceled(true); } disposables.add(session.onDidChange(e => { switch (e.status) { case TextToSpeechStatus.Started: - if (session === this._activeTextToSpeechSession) { - this.textToSpeechInProgress.set(true); - this._onDidStartTextToSpeechSession.fire(); - } + this.activeTextToSpeechSessions++; + this.textToSpeechInProgress.set(true); + this._onDidStartTextToSpeechSession.fire(); break; case TextToSpeechStatus.Stopped: - onSessionStoppedOrCanceled(); + onSessionStoppedOrCanceled(false); break; case TextToSpeechStatus.Error: this.logService.error(`Speech provider error in text to speech session: ${e.text}`); @@ -331,8 +331,8 @@ export class SpeechService extends Disposable implements ISpeechService { private readonly _onDidEndKeywordRecognition = this._register(new Emitter()); readonly onDidEndKeywordRecognition = this._onDidEndKeywordRecognition.event; - private _activeKeywordRecognitionSession: IKeywordRecognitionSession | undefined = undefined; - get hasActiveKeywordRecognition() { return !!this._activeKeywordRecognitionSession; } + private activeKeywordRecognitionSessions = 0; + get hasActiveKeywordRecognition() { return this.activeKeywordRecognitionSessions > 0; } async recognizeKeyword(token: CancellationToken): Promise { const result = new DeferredPromise(); @@ -399,16 +399,15 @@ export class SpeechService extends Disposable implements ISpeechService { private async doRecognizeKeyword(token: CancellationToken): Promise { const provider = await this.getProvider(); - const session = this._activeKeywordRecognitionSession = provider.createKeywordRecognitionSession(token); + const session = provider.createKeywordRecognitionSession(token); + this.activeKeywordRecognitionSessions++; this._onDidStartKeywordRecognition.fire(); const disposables = new DisposableStore(); const onSessionStoppedOrCanceled = () => { - if (session === this._activeKeywordRecognitionSession) { - this._activeKeywordRecognitionSession = undefined; - this._onDidEndKeywordRecognition.fire(); - } + this.activeKeywordRecognitionSessions--; + this._onDidEndKeywordRecognition.fire(); disposables.dispose(); }; diff --git a/src/vs/workbench/contrib/speech/common/speechService.ts b/src/vs/workbench/contrib/speech/common/speechService.ts index 4181ed15a63..4d39702b1e2 100644 --- a/src/vs/workbench/contrib/speech/common/speechService.ts +++ b/src/vs/workbench/contrib/speech/common/speechService.ts @@ -54,7 +54,7 @@ export interface ITextToSpeechEvent { export interface ITextToSpeechSession { readonly onDidChange: Event; - synthesize(text: string): void; + synthesize(text: string): Promise; } export enum KeywordRecognitionStatus { diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 34274cf1ee2..a965feab2b4 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -20,9 +20,10 @@ import { ITerminalProcessManager } from 'vs/workbench/contrib/terminal/common/te import { TerminalChatWidget } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget'; import { MarkdownString } from 'vs/base/common/htmlContent'; -import { ChatModel, ChatRequestModel, IChatRequestVariableData, getHistoryEntriesFromModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatModel, ChatRequestModel, IChatRequestVariableData, IChatResponseModel, getHistoryEntriesFromModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { TerminalChatContextKeys } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; +import { DeferredPromise } from 'vs/base/common/async'; const enum Message { NONE = 0, @@ -84,6 +85,10 @@ export class TerminalChatController extends Disposable implements ITerminalContr private readonly _model: MutableDisposable = this._register(new MutableDisposable()); + get scopedContextKeyService(): IContextKeyService { + return this._chatWidget?.value.inlineChatWidget.scopedContextKeyService ?? this._contextKeyService; + } + constructor( private readonly _instance: ITerminalInstance, processManager: ITerminalProcessManager, @@ -245,7 +250,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._requestActiveContextKey.reset(); } - async acceptInput(): Promise { + async acceptInput(): Promise { if (!this._model.value) { this._model.value = this._chatService.startSession(ChatAgentLocation.Terminal, CancellationToken.None); if (!this._model.value) { @@ -259,6 +264,16 @@ export class TerminalChatController extends Disposable implements ITerminalContr if (!this._lastInput) { return; } + + const responseCreated = new DeferredPromise(); + let responseCreatedComplete = false; + const completeResponseCreated = () => { + if (!responseCreatedComplete && this._currentRequest?.response) { + responseCreated.complete(this._currentRequest.response); + responseCreatedComplete = true; + } + }; + const accessibilityRequestId = this._chatAccessibilityService.acceptRequest(); this._requestActiveContextKey.set(true); const cancellationToken = new CancellationTokenSource().token; @@ -273,6 +288,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr } if (this._currentRequest) { model.acceptResponseProgress(this._currentRequest, progress); + completeResponseCreated(); } }; @@ -286,6 +302,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr variables: [] }; this._currentRequest = model.addRequest(request, requestVarData, 0); + completeResponseCreated(); const requestProps: IChatAgentRequest = { sessionId: model.sessionId, requestId: this._currentRequest!.id, @@ -310,6 +327,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._chatWidget?.value.inlineChatWidget.updateToolbar(true); if (this._currentRequest) { model.completeResponse(this._currentRequest); + completeResponseCreated(); } this._lastResponseContent = responseContent; if (this._currentRequest) { @@ -327,6 +345,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._responseSupportsIssueReportingContextKey.set(supportIssueReporting); } } + return responseCreated.p; } updateInput(text: string, selectAll = true): void { From 34925e183f66e6b69c36850d22b7d136f4b5d8ae Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 13 May 2024 11:28:12 +0200 Subject: [PATCH 117/357] report client target platform (#212584) --- .../extensionManagement/common/extensionManagement.ts | 1 + .../extensionManagement/node/extensionDownloader.ts | 5 +++-- .../node/extensionManagementService.ts | 5 +++-- .../node/extensionSignatureVerificationService.ts | 10 +++++++--- .../extensionManagementServerService.ts | 4 ---- .../remoteExtensionManagementService.ts | 5 +++-- 6 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index fa9e1b0983d..4249e27a561 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -19,6 +19,7 @@ export const WEB_EXTENSION_TAG = '__web_extension'; export const EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT = 'skipWalkthrough'; export const EXTENSION_INSTALL_SYNC_CONTEXT = 'extensionsSync'; export const EXTENSION_INSTALL_DEP_PACK_CONTEXT = 'dependecyOrPackExtensionInstall'; +export const EXTENSION_INSTALL_CLIENT_TARGET_PLATFORM_CONTEXT = 'clientTargetPlatform'; export interface IProductVersion { readonly version: string; diff --git a/src/vs/platform/extensionManagement/node/extensionDownloader.ts b/src/vs/platform/extensionManagement/node/extensionDownloader.ts index d670f821602..e8d8e6b1159 100644 --- a/src/vs/platform/extensionManagement/node/extensionDownloader.ts +++ b/src/vs/platform/extensionManagement/node/extensionDownloader.ts @@ -20,6 +20,7 @@ import { ExtensionVerificationStatus, toExtensionManagementError } from 'vs/plat import { ExtensionManagementError, ExtensionManagementErrorCode, IExtensionGalleryService, IGalleryExtension, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionKey, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionSignatureVerificationError, ExtensionSignatureVerificationCode, IExtensionSignatureVerificationService } from 'vs/platform/extensionManagement/node/extensionSignatureVerificationService'; +import { TargetPlatform } from 'vs/platform/extensions/common/extensions'; import { IFileService, IFileStatWithMetadata } from 'vs/platform/files/common/files'; import { ILogService } from 'vs/platform/log/common/log'; @@ -45,7 +46,7 @@ export class ExtensionsDownloader extends Disposable { this.cleanUpPromise = this.cleanUp(); } - async download(extension: IGalleryExtension, operation: InstallOperation, verifySignature: boolean): Promise<{ readonly location: URI; readonly verificationStatus: ExtensionVerificationStatus }> { + async download(extension: IGalleryExtension, operation: InstallOperation, verifySignature: boolean, clientTargetPlatform?: TargetPlatform): Promise<{ readonly location: URI; readonly verificationStatus: ExtensionVerificationStatus }> { await this.cleanUpPromise; const location = joinPath(this.extensionsDownloadDir, this.getName(extension)); @@ -73,7 +74,7 @@ export class ExtensionsDownloader extends Disposable { } try { - verificationStatus = await this.extensionSignatureVerificationService.verify(extension.identifier.id, location.fsPath, signatureArchiveLocation.fsPath); + verificationStatus = await this.extensionSignatureVerificationService.verify(extension.identifier.id, location.fsPath, signatureArchiveLocation.fsPath, clientTargetPlatform); } catch (error) { verificationStatus = (error as ExtensionSignatureVerificationError).code; if (verificationStatus === ExtensionSignatureVerificationCode.PackageIsInvalidZip || verificationStatus === ExtensionSignatureVerificationCode.SignatureArchiveIsInvalidZip) { diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 122a6ed9b7b..ebbd598ef88 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -29,7 +29,8 @@ import { AbstractExtensionManagementService, AbstractExtensionTask, ExtensionVer import { ExtensionManagementError, ExtensionManagementErrorCode, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService, IGalleryExtension, ILocalExtension, InstallOperation, Metadata, InstallOptions, - IProductVersion + IProductVersion, + EXTENSION_INSTALL_CLIENT_TARGET_PLATFORM_CONTEXT } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions, computeTargetPlatform, ExtensionKey, getGalleryExtensionId, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IExtensionsProfileScannerService, IScannedProfileExtension } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService'; @@ -967,7 +968,7 @@ export class InstallGalleryExtensionTask extends InstallExtensionTask { private async download(metadata: Metadata, token: CancellationToken): Promise<{ readonly location: URI; readonly verificationStatus: ExtensionVerificationStatus }> { try { - return await this.extensionsDownloader.download(this.gallery, this._operation, !this.options.donotVerifySignature); + return await this.extensionsDownloader.download(this.gallery, this._operation, !this.options.donotVerifySignature, this.options.context?.[EXTENSION_INSTALL_CLIENT_TARGET_PLATFORM_CONTEXT]); } catch (error) { this.logService.info(`Failed downloading. Retry again...`, this.gallery.identifier.id); type RetryDownloadingVSIXClassification = { diff --git a/src/vs/platform/extensionManagement/node/extensionSignatureVerificationService.ts b/src/vs/platform/extensionManagement/node/extensionSignatureVerificationService.ts index a76ca930005..1bfe311f29f 100644 --- a/src/vs/platform/extensionManagement/node/extensionSignatureVerificationService.ts +++ b/src/vs/platform/extensionManagement/node/extensionSignatureVerificationService.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { getErrorMessage } from 'vs/base/common/errors'; +import { TargetPlatform } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ILogService, LogLevel } from 'vs/platform/log/common/log'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -26,7 +27,7 @@ export interface IExtensionSignatureVerificationService { * @throws { ExtensionSignatureVerificationError } An error with a code indicating the validity, integrity, or trust issue * found during verification or a more fundamental issue (e.g.: a required dependency was not found). */ - verify(extensionId: string, vsixFilePath: string, signatureArchiveFilePath: string): Promise; + verify(extensionId: string, vsixFilePath: string, signatureArchiveFilePath: string, clientTargetPlatform?: TargetPlatform): Promise; } declare module vsceSign { @@ -106,7 +107,7 @@ export class ExtensionSignatureVerificationService implements IExtensionSignatur return this.moduleLoadingPromise; } - public async verify(extensionId: string, vsixFilePath: string, signatureArchiveFilePath: string): Promise { + public async verify(extensionId: string, vsixFilePath: string, signatureArchiveFilePath: string, clientTargetPlatform?: TargetPlatform): Promise { let module: typeof vsceSign; try { @@ -143,18 +144,21 @@ export class ExtensionSignatureVerificationService implements IExtensionSignatur code: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'result code of the verification' }; duration: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; 'isMeasurement': true; comment: 'amount of time taken to verify the signature' }; didExecute: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'whether the verification was executed' }; + clientTargetPlatform?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'target platform of the client' }; }; type ExtensionSignatureVerificationEvent = { extensionId: string; code: string; duration: number; didExecute: boolean; + clientTargetPlatform?: string; }; this.telemetryService.publicLog2('extensionsignature:verification', { extensionId, code: result.code, duration, - didExecute: result.didExecute + didExecute: result.didExecute, + clientTargetPlatform, }); if (result.code === ExtensionSignatureVerificationCode.Success) { diff --git a/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementServerService.ts b/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementServerService.ts index 69a430503f9..ccb40a9fbcc 100644 --- a/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementServerService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementServerService.ts @@ -14,10 +14,8 @@ import { NativeRemoteExtensionManagementService } from 'vs/workbench/services/ex import { ILabelService } from 'vs/platform/label/common/label'; import { IExtension } from 'vs/platform/extensions/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; import { NativeExtensionManagementService } from 'vs/workbench/services/extensionManagement/electron-sandbox/nativeExtensionManagementService'; import { Disposable } from 'vs/base/common/lifecycle'; -import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; export class ExtensionManagementServerService extends Disposable implements IExtensionManagementServerService { @@ -31,8 +29,6 @@ export class ExtensionManagementServerService extends Disposable implements IExt @ISharedProcessService sharedProcessService: ISharedProcessService, @IRemoteAgentService remoteAgentService: IRemoteAgentService, @ILabelService labelService: ILabelService, - @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService, - @IUserDataProfileService userDataProfileService: IUserDataProfileService, @IInstantiationService instantiationService: IInstantiationService, ) { super(); diff --git a/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts index 5e78b841010..79cf8b0ad4e 100644 --- a/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { ILocalExtension, IGalleryExtension, IExtensionGalleryService, InstallOperation, InstallOptions, ExtensionManagementError, ExtensionManagementErrorCode } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ILocalExtension, IGalleryExtension, IExtensionGalleryService, InstallOperation, InstallOptions, ExtensionManagementError, ExtensionManagementErrorCode, EXTENSION_INSTALL_CLIENT_TARGET_PLATFORM_CONTEXT } from 'vs/platform/extensionManagement/common/extensionManagement'; import { URI } from 'vs/base/common/uri'; import { ExtensionType, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; @@ -61,7 +61,8 @@ export class NativeRemoteExtensionManagementService extends RemoteExtensionManag return this.downloadAndInstall(extension, installOptions || {}); } try { - return await super.installFromGallery(extension, installOptions); + const clientTargetPlatform = await this.localExtensionManagementServer.extensionManagementService.getTargetPlatform(); + return await super.installFromGallery(extension, { ...installOptions, context: { ...installOptions?.context, [EXTENSION_INSTALL_CLIENT_TARGET_PLATFORM_CONTEXT]: clientTargetPlatform } }); } catch (error) { switch (error.name) { case ExtensionManagementErrorCode.Download: From 24d4616d5e2c97e922001bd680e5e859c21bc674 Mon Sep 17 00:00:00 2001 From: Dirk Baeumer Date: Mon, 13 May 2024 11:37:37 +0200 Subject: [PATCH 118/357] Make VS Code compile on Windows with NodeJS >=20 --- build/npm/postinstall.js | 1 + build/npm/preinstall.js | 9 +++++---- extensions/package.json | 3 +++ extensions/yarn.lock | 8 ++++---- package.json | 5 ++++- remote/package.json | 3 +++ remote/yarn.lock | 8 ++++---- yarn.lock | 8 ++++---- 8 files changed, 28 insertions(+), 17 deletions(-) diff --git a/build/npm/postinstall.js b/build/npm/postinstall.js index 72dd74f8986..bcac781e265 100644 --- a/build/npm/postinstall.js +++ b/build/npm/postinstall.js @@ -36,6 +36,7 @@ function yarnInstall(dir, opts) { ...(opts ?? {}), cwd: dir, stdio: 'inherit', + shell: true }; const raw = process.env['npm_config_argv'] || '{}'; diff --git a/build/npm/preinstall.js b/build/npm/preinstall.js index cfb5bb2985f..fdb01f579d6 100644 --- a/build/npm/preinstall.js +++ b/build/npm/preinstall.js @@ -99,7 +99,8 @@ function installHeaders() { const yarnResult = cp.spawnSync(yarn, ['install'], { env: process.env, cwd: path.join(__dirname, 'gyp'), - stdio: 'inherit' + stdio: 'inherit', + shell: true }); if (yarnResult.error || yarnResult.status !== 0) { console.error(`Installing node-gyp failed`); @@ -111,7 +112,7 @@ function installHeaders() { // file checked into our repository. So from that point it is save to construct the path // to that executable const node_gyp = path.join(__dirname, 'gyp', 'node_modules', '.bin', 'node-gyp.cmd'); - const result = cp.execFileSync(node_gyp, ['list'], { encoding: 'utf8' }); + const result = cp.execFileSync(node_gyp, ['list'], { encoding: 'utf8', shell: true }); const versions = new Set(result.split(/\n/g).filter(line => !line.startsWith('gyp info')).map(value => value)); const local = getHeaderInfo(path.join(__dirname, '..', '..', '.yarnrc')); @@ -119,7 +120,7 @@ function installHeaders() { if (local !== undefined && !versions.has(local.target)) { // Both disturl and target come from a file checked into our repository - cp.execFileSync(node_gyp, ['install', '--dist-url', local.disturl, local.target]); + cp.execFileSync(node_gyp, ['install', '--dist-url', local.disturl, local.target], { shell: true }); } // Avoid downloading headers for Windows arm64 till we move to Nodejs v19 in remote @@ -136,7 +137,7 @@ function installHeaders() { process.env['npm_config_arch'] !== "arm64" && process.arch !== "arm64") { // Both disturl and target come from a file checked into our repository - cp.execFileSync(node_gyp, ['install', '--dist-url', remote.disturl, remote.target]); + cp.execFileSync(node_gyp, ['install', '--dist-url', remote.disturl, remote.target], { shell: true }); } } diff --git a/extensions/package.json b/extensions/package.json index ef529f4ae68..2c83af40936 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -13,5 +13,8 @@ "@parcel/watcher": "2.1.0", "esbuild": "0.20.0", "vscode-grammar-updater": "^1.1.0" + }, + "resolutions": { + "node-gyp-build": "4.8.1" } } diff --git a/extensions/yarn.lock b/extensions/yarn.lock index a8cfe5c0351..fa4595ffa74 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -217,10 +217,10 @@ node-addon-api@^3.2.1: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== -node-gyp-build@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3" - integrity sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q== +node-gyp-build@4.8.1, node-gyp-build@^4.3.0: + version "4.8.1" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.1.tgz#976d3ad905e71b76086f4f0b0d3637fe79b6cda5" + integrity sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw== picomatch@^2.3.1: version "2.3.1" diff --git a/package.json b/package.json index 08597898b27..f7c6f37bbd0 100644 --- a/package.json +++ b/package.json @@ -216,6 +216,9 @@ "xml2js": "^0.5.0", "yaserver": "^0.4.0" }, + "resolutions": { + "node-gyp-build": "4.8.1" + }, "repository": { "type": "git", "url": "https://github.com/microsoft/vscode.git" @@ -226,4 +229,4 @@ "optionalDependencies": { "windows-foreground-love": "0.5.0" } -} \ No newline at end of file +} diff --git a/remote/package.json b/remote/package.json index c0397ce197f..3cdd8501efd 100644 --- a/remote/package.json +++ b/remote/package.json @@ -35,5 +35,8 @@ "vscode-textmate": "9.0.0", "yauzl": "^3.0.0", "yazl": "^2.4.3" + }, + "resolutions": { + "node-gyp-build": "4.8.1" } } diff --git a/remote/yarn.lock b/remote/yarn.lock index 88d688b5954..71e8b408a1f 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -421,10 +421,10 @@ node-addon-api@^4.3.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f" integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ== -node-gyp-build@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3" - integrity sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q== +node-gyp-build@4.8.1, node-gyp-build@^4.3.0: + version "4.8.1" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.1.tgz#976d3ad905e71b76086f4f0b0d3637fe79b6cda5" + integrity sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw== node-pty@1.1.0-beta11: version "1.1.0-beta11" diff --git a/yarn.lock b/yarn.lock index 3720c2fd112..ac573d403b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7266,10 +7266,10 @@ node-fetch@^2.6.0, node-fetch@^2.6.7: dependencies: whatwg-url "^5.0.0" -node-gyp-build@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3" - integrity sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q== +node-gyp-build@4.8.1, node-gyp-build@^4.3.0: + version "4.8.1" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.1.tgz#976d3ad905e71b76086f4f0b0d3637fe79b6cda5" + integrity sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw== node-html-markdown@^1.3.0: version "1.3.0" From 108e04580dcea02367c21d5188a0e91d8b1f3684 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 13 May 2024 12:09:07 +0200 Subject: [PATCH 119/357] Local history: does not preserve entries from previously deleted file (fix #212386) (#212537) --- .../browser/localHistoryTimeline.ts | 2 +- .../workingCopy/common/workingCopyHistory.ts | 6 + .../common/workingCopyHistoryService.ts | 96 ++++++++---- .../workingCopyHistoryService.test.ts | 137 ++++++++++++++++++ 4 files changed, 211 insertions(+), 30 deletions(-) diff --git a/src/vs/workbench/contrib/localHistory/browser/localHistoryTimeline.ts b/src/vs/workbench/contrib/localHistory/browser/localHistoryTimeline.ts index acd3ca80d4a..8d30e92f19e 100644 --- a/src/vs/workbench/contrib/localHistory/browser/localHistoryTimeline.ts +++ b/src/vs/workbench/contrib/localHistory/browser/localHistoryTimeline.ts @@ -151,7 +151,7 @@ export class LocalHistoryTimeline extends Disposable implements IWorkbenchContri return { handle: entry.id, label: SaveSourceRegistry.getSourceLabel(entry.source), - tooltip: new MarkdownString(`$(history) ${getLocalHistoryDateFormatter().format(entry.timestamp)}\n\n${SaveSourceRegistry.getSourceLabel(entry.source)}`, { supportThemeIcons: true }), + tooltip: new MarkdownString(`$(history) ${getLocalHistoryDateFormatter().format(entry.timestamp)}\n\n${SaveSourceRegistry.getSourceLabel(entry.source)}${entry.sourceDescription ? ` (${entry.sourceDescription})` : ``}`, { supportThemeIcons: true }), source: this.id, timestamp: entry.timestamp, themeIcon: LOCAL_HISTORY_ICON_ENTRY, diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyHistory.ts b/src/vs/workbench/services/workingCopy/common/workingCopyHistory.ts index 9430f116c9e..7c3a9a9af7e 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyHistory.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyHistory.ts @@ -48,6 +48,12 @@ export interface IWorkingCopyHistoryEntry { * Associated source with the history entry. */ source: SaveSource; + + /** + * Optional additional metadata associated with the + * source that can help to describe the source. + */ + sourceDescription: string | undefined; } export interface IWorkingCopyHistoryEntryDescriptor { diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyHistoryService.ts b/src/vs/workbench/services/workingCopy/common/workingCopyHistoryService.ts index 56e3fd5e9cd..4e0f74bdc15 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyHistoryService.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyHistoryService.ts @@ -28,7 +28,7 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { ILogService } from 'vs/platform/log/common/log'; import { SaveSource, SaveSourceRegistry } from 'vs/workbench/common/editor'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { lastOrDefault } from 'vs/base/common/arrays'; +import { distinct, lastOrDefault } from 'vs/base/common/arrays'; import { escapeRegExpCharacters } from 'vs/base/common/strings'; interface ISerializedWorkingCopyHistoryModel { @@ -41,9 +41,9 @@ interface ISerializedWorkingCopyHistoryModelEntry { readonly id: string; readonly timestamp: number; readonly source?: SaveSource; + readonly sourceDescription?: string; } - export interface IWorkingCopyHistoryModelOptions { /** @@ -119,7 +119,7 @@ export class WorkingCopyHistoryModel { return joinPath(historyHome, hash(workingCopyResource.toString()).toString(16)); } - async addEntry(source = WorkingCopyHistoryModel.FILE_SAVED_SOURCE, timestamp = Date.now(), token: CancellationToken): Promise { + async addEntry(source = WorkingCopyHistoryModel.FILE_SAVED_SOURCE, sourceDescription: string | undefined = undefined, timestamp = Date.now(), token: CancellationToken): Promise { let entryToReplace: IWorkingCopyHistoryEntry | undefined = undefined; // Figure out if the last entry should be replaced based @@ -138,12 +138,12 @@ export class WorkingCopyHistoryModel { // Replace lastest entry in history if (entryToReplace) { - entry = await this.doReplaceEntry(entryToReplace, timestamp, token); + entry = await this.doReplaceEntry(entryToReplace, source, sourceDescription, timestamp, token); } // Add entry to history else { - entry = await this.doAddEntry(source, timestamp, token); + entry = await this.doAddEntry(source, sourceDescription, timestamp, token); } // Flush now if configured @@ -154,7 +154,7 @@ export class WorkingCopyHistoryModel { return entry; } - private async doAddEntry(source: SaveSource, timestamp: number, token: CancellationToken): Promise { + private async doAddEntry(source: SaveSource, sourceDescription: string | undefined = undefined, timestamp: number, token: CancellationToken): Promise { const workingCopyResource = assertIsDefined(this.workingCopyResource); const workingCopyName = assertIsDefined(this.workingCopyName); const historyEntriesFolder = assertIsDefined(this.historyEntriesFolder); @@ -170,7 +170,8 @@ export class WorkingCopyHistoryModel { workingCopy: { resource: workingCopyResource, name: workingCopyName }, location, timestamp, - source + source, + sourceDescription }; this.entries.push(entry); @@ -183,13 +184,15 @@ export class WorkingCopyHistoryModel { return entry; } - private async doReplaceEntry(entry: IWorkingCopyHistoryEntry, timestamp: number, token: CancellationToken): Promise { + private async doReplaceEntry(entry: IWorkingCopyHistoryEntry, source: SaveSource, sourceDescription: string | undefined = undefined, timestamp: number, token: CancellationToken): Promise { const workingCopyResource = assertIsDefined(this.workingCopyResource); // Perform a fast clone operation with minimal overhead to the existing location await this.fileService.cloneFile(workingCopyResource, entry.location); // Update entry + entry.source = source; + entry.sourceDescription = sourceDescription; entry.timestamp = timestamp; // Update version ID of model to use for storing later @@ -335,7 +338,8 @@ export class WorkingCopyHistoryModel { workingCopy: { resource: workingCopyResource, name: workingCopyName }, location: entryStat.resource, timestamp: entryStat.mtime, - source: WorkingCopyHistoryModel.FILE_SAVED_SOURCE + source: WorkingCopyHistoryModel.FILE_SAVED_SOURCE, + sourceDescription: undefined }); } } @@ -348,7 +352,8 @@ export class WorkingCopyHistoryModel { entries.set(entry.id, { ...existingEntry, timestamp: entry.timestamp, - source: entry.source ?? existingEntry.source + source: entry.source ?? existingEntry.source, + sourceDescription: entry.sourceDescription ?? existingEntry.sourceDescription }); } } @@ -357,31 +362,58 @@ export class WorkingCopyHistoryModel { return entries; } - async moveEntries(targetWorkingCopyResource: URI, source: SaveSource, token: CancellationToken): Promise { + async moveEntries(target: WorkingCopyHistoryModel, source: SaveSource, token: CancellationToken): Promise { + const timestamp = Date.now(); + const sourceDescription = this.labelService.getUriLabel(assertIsDefined(this.workingCopyResource)); - // Ensure model stored so that any pending data is flushed - await this.store(token); + // Move all entries into the target folder so that we preserve + // any existing history entries that might already be present - if (token.isCancellationRequested) { - return undefined; - } - - // Rename existing entries folder const sourceHistoryEntriesFolder = assertIsDefined(this.historyEntriesFolder); - const targetHistoryFolder = this.toHistoryEntriesFolder(this.historyHome, targetWorkingCopyResource); + const targetHistoryEntriesFolder = assertIsDefined(target.historyEntriesFolder); try { - await this.fileService.move(sourceHistoryEntriesFolder, targetHistoryFolder, true); + for (const entry of this.entries) { + await this.fileService.move(entry.location, joinPath(targetHistoryEntriesFolder, entry.id), true); + } + await this.fileService.del(sourceHistoryEntriesFolder, { recursive: true }); } catch (error) { - if (!(error instanceof FileOperationError && error.fileOperationResult === FileOperationResult.FILE_NOT_FOUND)) { - this.traceError(error); + if (!this.isFileNotFound(error)) { + try { + // In case of an error (unless not found), fallback to moving the entire folder + await this.fileService.move(sourceHistoryEntriesFolder, targetHistoryEntriesFolder, true); + } catch (error) { + if (!this.isFileNotFound(error)) { + this.traceError(error); + } + } } } + // Merge our entries with target entries before updating associated working copy + const allEntries = distinct([...this.entries, ...target.entries], entry => entry.id).sort((entryA, entryB) => entryA.timestamp - entryB.timestamp); + // Update our associated working copy + const targetWorkingCopyResource = assertIsDefined(target.workingCopyResource); this.setWorkingCopy(targetWorkingCopyResource); + // Restore our entries and ensure correct metadata + const targetWorkingCopyName = assertIsDefined(target.workingCopyName); + for (const entry of allEntries) { + this.entries.push({ + id: entry.id, + location: joinPath(targetHistoryEntriesFolder, entry.id), + source: entry.source, + sourceDescription: entry.sourceDescription, + timestamp: entry.timestamp, + workingCopy: { + resource: targetWorkingCopyResource, + name: targetWorkingCopyName + } + }); + } + // Add entry for the move - await this.addEntry(source, undefined, token); + await this.addEntry(source, sourceDescription, timestamp, token); // Store model again to updated location await this.store(token); @@ -483,6 +515,7 @@ export class WorkingCopyHistoryModel { return { id: entry.id, source: entry.source !== WorkingCopyHistoryModel.FILE_SAVED_SOURCE ? entry.source : undefined, + sourceDescription: entry.sourceDescription, timestamp: entry.timestamp }; }) @@ -498,7 +531,7 @@ export class WorkingCopyHistoryModel { try { serializedModel = JSON.parse((await this.fileService.readFile(historyEntriesListingFile)).value.toString()); } catch (error) { - if (!(error instanceof FileOperationError && error.fileOperationResult === FileOperationResult.FILE_NOT_FOUND)) { + if (!this.isFileNotFound(error)) { this.traceError(error); } } @@ -516,7 +549,7 @@ export class WorkingCopyHistoryModel { try { rawEntries = (await this.fileService.resolve(historyEntriesFolder, { resolveMetadata: true })).children; } catch (error) { - if (!(error instanceof FileOperationError && error.fileOperationResult === FileOperationResult.FILE_NOT_FOUND)) { + if (!this.isFileNotFound(error)) { this.traceError(error); } } @@ -532,6 +565,10 @@ export class WorkingCopyHistoryModel { ); } + private isFileNotFound(error: unknown): boolean { + return error instanceof FileOperationError && error.fileOperationResult === FileOperationResult.FILE_NOT_FOUND; + } + private traceError(error: Error): void { this.logService.trace('[Working Copy History Service]', error); } @@ -644,14 +681,15 @@ export abstract class WorkingCopyHistoryService extends Disposable implements IW return resources; } - private async doMoveEntries(model: WorkingCopyHistoryModel, source: SaveSource, sourceWorkingCopyResource: URI, targetWorkingCopyResource: URI): Promise { + private async doMoveEntries(source: WorkingCopyHistoryModel, saveSource: SaveSource, sourceWorkingCopyResource: URI, targetWorkingCopyResource: URI): Promise { // Move to target via model - await model.moveEntries(targetWorkingCopyResource, source, CancellationToken.None); + const target = await this.getModel(targetWorkingCopyResource); + await source.moveEntries(target, saveSource, CancellationToken.None); // Update model in our map this.models.delete(sourceWorkingCopyResource); - this.models.set(targetWorkingCopyResource, model); + this.models.set(targetWorkingCopyResource, source); return targetWorkingCopyResource; } @@ -668,7 +706,7 @@ export abstract class WorkingCopyHistoryService extends Disposable implements IW } // Add to model - return model.addEntry(source, timestamp, token); + return model.addEntry(source, undefined, timestamp, token); } async updateEntry(entry: IWorkingCopyHistoryEntry, properties: { source: SaveSource }, token: CancellationToken): Promise { diff --git a/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyHistoryService.test.ts b/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyHistoryService.test.ts index ff4f48a4af9..40a3f059f9b 100644 --- a/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyHistoryService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyHistoryService.test.ts @@ -674,12 +674,14 @@ suite('WorkingCopyHistoryService', () => { assert.strictEqual(entries[0].id, entry1.id); assert.strictEqual(entries[0].timestamp, entry1.timestamp); assert.strictEqual(entries[0].source, entry1.source); + assert.ok(!entries[0].sourceDescription); assert.notStrictEqual(entries[0].location, entry1.location); assert.strictEqual(entries[0].workingCopy.resource.toString(), renamedWorkingCopyResource.toString()); assert.strictEqual(entries[1].id, entry2.id); assert.strictEqual(entries[1].timestamp, entry2.timestamp); assert.strictEqual(entries[1].source, entry2.source); + assert.ok(!entries[1].sourceDescription); assert.notStrictEqual(entries[1].location, entry2.location); assert.strictEqual(entries[1].workingCopy.resource.toString(), renamedWorkingCopyResource.toString()); @@ -688,6 +690,10 @@ suite('WorkingCopyHistoryService', () => { assert.strictEqual(entries[2].source, entry3.source); assert.notStrictEqual(entries[2].location, entry3.location); assert.strictEqual(entries[2].workingCopy.resource.toString(), renamedWorkingCopyResource.toString()); + assert.ok(!entries[2].sourceDescription); + + assert.strictEqual(entries[3].source, 'renamed.source' /* for the move */); + assert.ok(entries[3].sourceDescription); // contains the source working copy path const all = await service.getAll(CancellationToken.None); assert.strictEqual(all.length, 1); @@ -774,6 +780,9 @@ suite('WorkingCopyHistoryService', () => { assert.notStrictEqual(entries[2].location, entry3B.location); assert.strictEqual(entries[2].workingCopy.resource.toString(), renamedWorkingCopy2Resource.toString()); + assert.strictEqual(entries[3].source, 'moved.source' /* for the move */); + assert.ok(entries[3].sourceDescription); // contains the source working copy path + const all = await service.getAll(CancellationToken.None); assert.strictEqual(all.length, 2); for (const resource of all) { @@ -783,5 +792,133 @@ suite('WorkingCopyHistoryService', () => { } }); + test('move entries (file rename) - preserves previous entries (no new entries)', async () => { + const workingCopyTarget = disposables.add(new TestWorkingCopy(testFile1Path)); + const workingCopySource = disposables.add(new TestWorkingCopy(testFile2Path)); + + const entry1 = await addEntry({ resource: workingCopyTarget.resource, source: 'test-source1' }, CancellationToken.None); + const entry2 = await addEntry({ resource: workingCopyTarget.resource, source: 'test-source2' }, CancellationToken.None); + const entry3 = await addEntry({ resource: workingCopyTarget.resource, source: 'test-source3' }, CancellationToken.None); + + let entries = await service.getEntries(workingCopyTarget.resource, CancellationToken.None); + assert.strictEqual(entries.length, 3); + + entries = await service.getEntries(workingCopySource.resource, CancellationToken.None); + assert.strictEqual(entries.length, 0); + + await fileService.move(workingCopySource.resource, workingCopyTarget.resource, true); + + const result = await service.moveEntries(workingCopySource.resource, workingCopyTarget.resource); + + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].toString(), workingCopyTarget.resource.toString()); + + entries = await service.getEntries(workingCopySource.resource, CancellationToken.None); + assert.strictEqual(entries.length, 0); + + entries = await service.getEntries(workingCopyTarget.resource, CancellationToken.None); + assert.strictEqual(entries.length, 4); + + assert.strictEqual(entries[0].id, entry1.id); + assert.strictEqual(entries[0].timestamp, entry1.timestamp); + assert.strictEqual(entries[0].source, entry1.source); + assert.notStrictEqual(entries[0].location, entry1.location); + assert.strictEqual(entries[0].workingCopy.resource.toString(), workingCopyTarget.resource.toString()); + + assert.strictEqual(entries[1].id, entry2.id); + assert.strictEqual(entries[1].timestamp, entry2.timestamp); + assert.strictEqual(entries[1].source, entry2.source); + assert.notStrictEqual(entries[1].location, entry2.location); + assert.strictEqual(entries[1].workingCopy.resource.toString(), workingCopyTarget.resource.toString()); + + assert.strictEqual(entries[2].id, entry3.id); + assert.strictEqual(entries[2].timestamp, entry3.timestamp); + assert.strictEqual(entries[2].source, entry3.source); + assert.notStrictEqual(entries[2].location, entry3.location); + assert.strictEqual(entries[2].workingCopy.resource.toString(), workingCopyTarget.resource.toString()); + + assert.strictEqual(entries[3].source, 'renamed.source' /* for the move */); + assert.ok(entries[3].sourceDescription); // contains the source working copy path + + const all = await service.getAll(CancellationToken.None); + assert.strictEqual(all.length, 1); + assert.strictEqual(all[0].toString(), workingCopyTarget.resource.toString()); + }); + + test('move entries (file rename) - preserves previous entries (new entries)', async () => { + const workingCopyTarget = disposables.add(new TestWorkingCopy(testFile1Path)); + const workingCopySource = disposables.add(new TestWorkingCopy(testFile2Path)); + + const targetEntry1 = await addEntry({ resource: workingCopyTarget.resource, source: 'test-target1' }, CancellationToken.None); + const targetEntry2 = await addEntry({ resource: workingCopyTarget.resource, source: 'test-target2' }, CancellationToken.None); + const targetEntry3 = await addEntry({ resource: workingCopyTarget.resource, source: 'test-target3' }, CancellationToken.None); + + const sourceEntry1 = await addEntry({ resource: workingCopySource.resource, source: 'test-source1' }, CancellationToken.None); + const sourceEntry2 = await addEntry({ resource: workingCopySource.resource, source: 'test-source2' }, CancellationToken.None); + const sourceEntry3 = await addEntry({ resource: workingCopySource.resource, source: 'test-source3' }, CancellationToken.None); + + let entries = await service.getEntries(workingCopyTarget.resource, CancellationToken.None); + assert.strictEqual(entries.length, 3); + + entries = await service.getEntries(workingCopySource.resource, CancellationToken.None); + assert.strictEqual(entries.length, 3); + + await fileService.move(workingCopySource.resource, workingCopyTarget.resource, true); + + const result = await service.moveEntries(workingCopySource.resource, workingCopyTarget.resource); + + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].toString(), workingCopyTarget.resource.toString()); + + entries = await service.getEntries(workingCopySource.resource, CancellationToken.None); + assert.strictEqual(entries.length, 0); + + entries = await service.getEntries(workingCopyTarget.resource, CancellationToken.None); + assert.strictEqual(entries.length, 7); + + assert.strictEqual(entries[0].id, targetEntry1.id); + assert.strictEqual(entries[0].timestamp, targetEntry1.timestamp); + assert.strictEqual(entries[0].source, targetEntry1.source); + assert.notStrictEqual(entries[0].location, targetEntry1.location); + assert.strictEqual(entries[0].workingCopy.resource.toString(), workingCopyTarget.resource.toString()); + + assert.strictEqual(entries[1].id, targetEntry2.id); + assert.strictEqual(entries[1].timestamp, targetEntry2.timestamp); + assert.strictEqual(entries[1].source, targetEntry2.source); + assert.notStrictEqual(entries[1].location, targetEntry2.location); + assert.strictEqual(entries[1].workingCopy.resource.toString(), workingCopyTarget.resource.toString()); + + assert.strictEqual(entries[2].id, targetEntry3.id); + assert.strictEqual(entries[2].timestamp, targetEntry3.timestamp); + assert.strictEqual(entries[2].source, targetEntry3.source); + assert.notStrictEqual(entries[2].location, targetEntry3.location); + assert.strictEqual(entries[2].workingCopy.resource.toString(), workingCopyTarget.resource.toString()); + + assert.strictEqual(entries[3].id, sourceEntry1.id); + assert.strictEqual(entries[3].timestamp, sourceEntry1.timestamp); + assert.strictEqual(entries[3].source, sourceEntry1.source); + assert.notStrictEqual(entries[3].location, sourceEntry1.location); + assert.strictEqual(entries[3].workingCopy.resource.toString(), workingCopyTarget.resource.toString()); + + assert.strictEqual(entries[4].id, sourceEntry2.id); + assert.strictEqual(entries[4].timestamp, sourceEntry2.timestamp); + assert.strictEqual(entries[4].source, sourceEntry2.source); + assert.notStrictEqual(entries[4].location, sourceEntry2.location); + assert.strictEqual(entries[4].workingCopy.resource.toString(), workingCopyTarget.resource.toString()); + + assert.strictEqual(entries[5].id, sourceEntry3.id); + assert.strictEqual(entries[5].timestamp, sourceEntry3.timestamp); + assert.strictEqual(entries[5].source, sourceEntry3.source); + assert.notStrictEqual(entries[5].location, sourceEntry3.location); + assert.strictEqual(entries[5].workingCopy.resource.toString(), workingCopyTarget.resource.toString()); + + assert.strictEqual(entries[6].source, 'renamed.source' /* for the move */); + assert.ok(entries[6].sourceDescription); // contains the source working copy path + + const all = await service.getAll(CancellationToken.None); + assert.strictEqual(all.length, 1); + assert.strictEqual(all[0].toString(), workingCopyTarget.resource.toString()); + }); + ensureNoDisposablesAreLeakedInTestSuite(); }); From 0c5a7bd24e2c102f9a7e268b27e0f95345cae013 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 13 May 2024 12:10:02 +0200 Subject: [PATCH 120/357] language model API tweak (#212588) * add `LanguageModelChatMessage#User/Assistant` utils, * rename computeTokenLength to `countTokens` * jsdoc and todo updates --- .../api/common/extHostLanguageModels.ts | 2 +- src/vs/workbench/api/common/extHostTypes.ts | 8 ++ .../vscode.proposed.languageModels.d.ts | 110 +++++++++--------- 3 files changed, 65 insertions(+), 55 deletions(-) diff --git a/src/vs/workbench/api/common/extHostLanguageModels.ts b/src/vs/workbench/api/common/extHostLanguageModels.ts index 0bb8bbf1a52..e4145c224ca 100644 --- a/src/vs/workbench/api/common/extHostLanguageModels.ts +++ b/src/vs/workbench/api/common/extHostLanguageModels.ts @@ -262,7 +262,7 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { version: data.metadata.version, name: data.metadata.name, contextSize: data.metadata.tokens, - computeTokenLength(text, token) { + countTokens(text, token) { if (!that._allLanguageModelData.has(identifier)) { throw extHostTypes.LanguageModelError.NotFound(identifier); } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index e0dc6404965..4112f9a4a0c 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -4453,6 +4453,14 @@ export enum LanguageModelChatMessageRole { export class LanguageModelChatMessage implements vscode.LanguageModelChatMessage { + static User(content: string, name?: string): LanguageModelChatMessage { + return new LanguageModelChatMessage(LanguageModelChatMessageRole.User, content, name); + } + + static Assistant(content: string, name?: string): LanguageModelChatMessage { + return new LanguageModelChatMessage(LanguageModelChatMessageRole.Assistant, content, name); + } + role: vscode.LanguageModelChatMessageRole; content: string; name: string | undefined; diff --git a/src/vscode-dts/vscode.proposed.languageModels.d.ts b/src/vscode-dts/vscode.proposed.languageModels.d.ts index 85ee736cb6f..11c0e2ade17 100644 --- a/src/vscode-dts/vscode.proposed.languageModels.d.ts +++ b/src/vscode-dts/vscode.proposed.languageModels.d.ts @@ -8,49 +8,7 @@ declare module 'vscode' { // https://github.com/microsoft/vscode/issues/206265 /** - * Represents a language model response. - * - * @see {@link LanguageModelAccess.chatRequest} - */ - export interface LanguageModelChatResponse { - - /** - * An async iterable that is a stream of text chunks forming the overall response. - * - * *Note* that this stream will error when during data receiving an error occurs. Consumers of - * the stream should handle the errors accordingly. - * - * @example - * ```ts - * try { - * // consume stream - * for await (const chunk of response.stream) { - * console.log(chunk); - * } - * - * } catch(e) { - * // stream ended with an error - * console.error(e); - * } - * ``` - */ - stream: AsyncIterable; - } - - //TODO@API give this some structure - // https://github.com/openai/openai-openapi/blob/master/openapi.yaml#L7700, https://platform.openai.com/docs/guides/text-generation/chat-completions-api - // https://github.com/ollama/ollama/blob/main/docs/api.md#response-7 - // https://docs.anthropic.com/claude/reference/messages_post - export interface LanguageModelChatResponse2 { - - message: { - role: LanguageModelChatMessageRole.Assistant; - content: AsyncIterable; - }; - } - - /** - * Represents the role of a chat message. This is either the user or the assistant/model. + * Represents the role of a chat message. This is either the user or the assistant. */ export enum LanguageModelChatMessageRole { /** @@ -68,6 +26,23 @@ declare module 'vscode' { * Represents a message in a chat. Can assume different roles, like user or assistant. */ export class LanguageModelChatMessage { + + /** + * Utility to create a new user message. + * + * @param content The content of the message. + * @param name The optional name of a user for the message. + */ + static User(content: string, name?: string): LanguageModelChatMessage; + + /** + * Utility to create a new assistant message. + * + * @param content The content of the message. + * @param name The optional name of a user for the message. + */ + static Assistant(content: string, name?: string): LanguageModelChatMessage; + /** * The role of this message. */ @@ -93,12 +68,42 @@ declare module 'vscode' { constructor(role: LanguageModelChatMessageRole, content: string, name?: string); } + /** + * Represents a language model response. + * + * @see {@link LanguageModelAccess.chatRequest} + */ + // TODO@API add something like `modelResult: Thenable<{ [name: string]: any }>` + export interface LanguageModelChatResponse { + + /** + * An async iterable that is a stream of text chunks forming the overall response. + * + * *Note* that this stream will error when during data receiving an error occurs. Consumers of + * the stream should handle the errors accordingly. + * + * @example + * ```ts + * try { + * // consume stream + * for await (const chunk of response.stream) { + * console.log(chunk); + * } + * + * } catch(e) { + * // stream ended with an error + * console.error(e); + * } + * ``` + */ + stream: AsyncIterable; + } + /** * Represents a language model for making chat requests. * * @see {@link lm.selectChatModels} */ - // TODO@API name LanguageModelChatEndpoint export interface LanguageModelChat { /** * Opaque identifier of the language model. @@ -124,7 +129,7 @@ declare module 'vscode' { /** * Opaque version string of the model. This is defined by the extension contributing the language model - * and subject to change while the identifier is stable. + * and subject to change. */ readonly version: string; @@ -158,16 +163,13 @@ declare module 'vscode' { sendRequest(messages: LanguageModelChatMessage[], options?: LanguageModelChatRequestOptions, token?: CancellationToken): Thenable; /** - * Uses the model specific tokenzier and computes the length in tokens of a given message. - * + * Count the number of tokens in a message using the model specific tokenizer-logic. + * @param text A string or a message instance. - * @param token Optional cancellation token. - * @returns A thenable that resolves to the length of the message in tokens. + * @param token Optional cancellation token. See {@link CancellationTokenSource} for how to create one. + * @returns A thenable that resolves to the number of tokens. */ - // TODO@API `undefined` when the language model does not support computing token length - // ollama has nothing - // anthropic suggests to count after the fact https://github.com/anthropics/anthropic-tokenizer-typescript?tab=readme-ov-file#anthropic-typescript-tokenizer - computeTokenLength(text: string | LanguageModelChatMessage, token?: CancellationToken): Thenable; + countTokens(text: string | LanguageModelChatMessage, token?: CancellationToken): Thenable; } /** @@ -295,7 +297,7 @@ declare module 'vscode' { * * *Note* that calling this function will not trigger a consent UI but just checks. * - * @param languageModelId A language model identifier. + * @param languageModelId A language model identifier, see {@link LanguageModelChat.id} * @return `true` if a request can be made, `false` if not, `undefined` if the language * model does not exist or consent hasn't been asked for. */ From 98e776cfba3994128e8e1745332cf98b64ea6d96 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 13 May 2024 12:20:23 +0200 Subject: [PATCH 121/357] debt - more `readonly` in editor group model (#212589) --- src/vs/workbench/common/editor/editorGroupModel.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/common/editor/editorGroupModel.ts b/src/vs/workbench/common/editor/editorGroupModel.ts index 60047f630e3..30c3ef5e9ca 100644 --- a/src/vs/workbench/common/editor/editorGroupModel.ts +++ b/src/vs/workbench/common/editor/editorGroupModel.ts @@ -216,10 +216,10 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { private locked = false; - private preview: EditorInput | null = null; // editor in preview state - private active: EditorInput | null = null; // editor in active state - private sticky = -1; // index of first editor in sticky state - private transient = new Set(); // editors in transient state + private preview: EditorInput | null = null; // editor in preview state + private active: EditorInput | null = null; // editor in active state + private sticky = -1; // index of first editor in sticky state + private readonly transient = new Set(); // editors in transient state private editorOpenPositioning: ('left' | 'right' | 'first' | 'last') | undefined; private focusRecentEditorAfterClose: boolean | undefined; From 4b1cc22d870a4b46f1aeec76b65e3f38a2549dca Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Mon, 13 May 2024 12:40:00 +0200 Subject: [PATCH 122/357] Revert "testing: polish test coverage bar" (#212593) --- src/vs/base/browser/dom.ts | 2 +- src/vs/editor/browser/editorBrowser.ts | 10 - src/vs/editor/browser/view.ts | 3 +- .../overlayWidgets/overlayWidgets.ts | 46 +-- .../widget/diffEditor/diffEditorWidget.ts | 3 +- .../editor/browser/widget/diffEditor/utils.ts | 22 +- .../stickyScroll/browser/stickyScroll.css | 1 - .../browser/stickyScrollWidget.ts | 5 +- src/vs/monaco.d.ts | 9 - .../browser/accessibilitySignalService.ts | 15 +- .../common/platformObservableUtils.ts | 30 -- .../mergeEditor/browser/model/diffComputer.ts | 2 +- .../contrib/mergeEditor/browser/utils.ts | 13 +- .../browser/view/editors/codeEditorView.ts | 3 +- .../mergeEditor/browser/view/mergeEditor.ts | 3 +- .../mergeEditor/browser/view/viewModel.ts | 2 +- .../browser/codeCoverageDecorations.ts | 299 ++++-------------- .../contrib/testing/browser/media/testing.css | 50 +-- .../testing/browser/testCoverageView.ts | 2 - .../contrib/testing/common/configuration.ts | 7 - .../contrib/testing/common/constants.ts | 1 - .../testing/common/testCoverageService.ts | 33 +- .../testing/common/testingContextKeys.ts | 1 - .../textMateWorkerTokenizerController.ts | 13 +- 24 files changed, 183 insertions(+), 392 deletions(-) delete mode 100644 src/vs/platform/observable/common/platformObservableUtils.ts diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 76abbb844ec..ff113c9baa9 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -188,7 +188,7 @@ function _wrapAsStandardKeyboardEvent(handler: (e: IKeyboardEvent) => void): (e: export const addStandardDisposableListener: IAddStandardDisposableListenerSignature = function addStandardDisposableListener(node: HTMLElement, type: string, handler: (event: any) => void, useCapture?: boolean): IDisposable { let wrapHandler = handler; - if (type === 'click' || type === 'mousedown' || type === 'contextmenu') { + if (type === 'click' || type === 'mousedown') { wrapHandler = _wrapAsStandardMouseEvent(getWindow(node), handler); } else if (type === 'keydown' || type === 'keypress' || type === 'keyup') { wrapHandler = _wrapAsStandardKeyboardEvent(handler); diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index 2985f959335..cbaa5f01d3b 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -250,21 +250,11 @@ export interface IOverlayWidgetPosition { * The position preference for the overlay widget. */ preference: OverlayWidgetPositionPreference | IOverlayWidgetPositionCoordinates | null; - - /** - * When set, stacks with other overlay widgets with the same preference, - * in an order determined by the ordinal value. - */ - stackOridinal?: number; } /** * An overlay widgets renders on top of the text. */ export interface IOverlayWidget { - /** - * Event fired when the widget layout changes. - */ - onDidLayout?: Event; /** * Render this overlay widget in a location where it could overflow the editor's view dom node. */ diff --git a/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts index 5558cf28cd4..a803234c4b4 100644 --- a/src/vs/editor/browser/view.ts +++ b/src/vs/editor/browser/view.ts @@ -634,7 +634,8 @@ export class View extends ViewEventHandler { } public layoutOverlayWidget(widgetData: IOverlayWidgetData): void { - const shouldRender = this._overlayWidgets.setWidgetPosition(widgetData.widget, widgetData.position); + const newPreference = widgetData.position ? widgetData.position.preference : null; + const shouldRender = this._overlayWidgets.setWidgetPosition(widgetData.widget, newPreference); if (shouldRender) { this._scheduleRender(); } diff --git a/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts b/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts index 6187175e028..0953248e2ab 100644 --- a/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts +++ b/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts @@ -5,7 +5,7 @@ import 'vs/css!./overlayWidgets'; import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; -import { IOverlayWidget, IOverlayWidgetPosition, IOverlayWidgetPositionCoordinates, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; +import { IOverlayWidget, IOverlayWidgetPositionCoordinates, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; import { PartFingerprint, PartFingerprints, ViewPart } from 'vs/editor/browser/view/viewPart'; import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/browser/view/renderingContext'; import { ViewContext } from 'vs/editor/common/viewModel/viewContext'; @@ -17,7 +17,6 @@ import * as dom from 'vs/base/browser/dom'; interface IWidgetData { widget: IOverlayWidget; preference: OverlayWidgetPositionPreference | IOverlayWidgetPositionCoordinates | null; - stack?: number; domNode: FastDomNode; } @@ -110,17 +109,14 @@ export class ViewOverlayWidgets extends ViewPart { this._updateMaxMinWidth(); } - public setWidgetPosition(widget: IOverlayWidget, position: IOverlayWidgetPosition | null): boolean { + public setWidgetPosition(widget: IOverlayWidget, preference: OverlayWidgetPositionPreference | IOverlayWidgetPositionCoordinates | null): boolean { const widgetData = this._widgets[widget.getId()]; - const preference = position ? position.preference : null; - const stack = position?.stackOridinal; - if (widgetData.preference === preference && widgetData.stack === stack) { + if (widgetData.preference === preference) { this._updateMaxMinWidth(); return false; } widgetData.preference = preference; - widgetData.stack = stack; this.setShouldRender(); this._updateMaxMinWidth(); @@ -154,7 +150,7 @@ export class ViewOverlayWidgets extends ViewPart { this._context.viewLayout.setOverlayWidgetsMinWidth(maxMinWidth); } - private _renderWidget(widgetData: IWidgetData, stackCoordinates: number[]): void { + private _renderWidget(widgetData: IWidgetData): void { const domNode = widgetData.domNode; if (widgetData.preference === null) { @@ -162,29 +158,16 @@ export class ViewOverlayWidgets extends ViewPart { return; } - const maxRight = (2 * this._verticalScrollbarWidth) + this._minimapWidth; - if (widgetData.preference === OverlayWidgetPositionPreference.TOP_RIGHT_CORNER || widgetData.preference === OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER) { - if (widgetData.preference === OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER) { - domNode.setTop(0); - } else { - const widgetHeight = domNode.domNode.clientHeight; - domNode.setTop((this._editorHeight - widgetHeight - 2 * this._horizontalScrollbarHeight)); - } - - if (widgetData.stack !== undefined) { - domNode.setTop(stackCoordinates[widgetData.preference]); - stackCoordinates[widgetData.preference] += domNode.domNode.clientWidth; - } else { - domNode.setRight(maxRight); - } + if (widgetData.preference === OverlayWidgetPositionPreference.TOP_RIGHT_CORNER) { + domNode.setTop(0); + domNode.setRight((2 * this._verticalScrollbarWidth) + this._minimapWidth); + } else if (widgetData.preference === OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER) { + const widgetHeight = domNode.domNode.clientHeight; + domNode.setTop((this._editorHeight - widgetHeight - 2 * this._horizontalScrollbarHeight)); + domNode.setRight((2 * this._verticalScrollbarWidth) + this._minimapWidth); } else if (widgetData.preference === OverlayWidgetPositionPreference.TOP_CENTER) { + domNode.setTop(0); domNode.domNode.style.right = '50%'; - if (widgetData.stack !== undefined) { - domNode.setTop(stackCoordinates[OverlayWidgetPositionPreference.TOP_CENTER]); - stackCoordinates[OverlayWidgetPositionPreference.TOP_CENTER] += domNode.domNode.clientHeight; - } else { - domNode.setTop(0); - } } else { const { top, left } = widgetData.preference; const fixedOverflowWidgets = this._context.configuration.options.get(EditorOption.fixedOverflowWidgets); @@ -211,12 +194,9 @@ export class ViewOverlayWidgets extends ViewPart { this._domNode.setWidth(this._editorWidth); const keys = Object.keys(this._widgets); - const stackCoordinates = Array.from({ length: OverlayWidgetPositionPreference.TOP_CENTER + 1 }, () => 0); - keys.sort((a, b) => (this._widgets[a].stack || 0) - (this._widgets[b].stack || 0)); - for (let i = 0, len = keys.length; i < len; i++) { const widgetId = keys[i]; - this._renderWidget(this._widgets[widgetId], stackCoordinates); + this._renderWidget(this._widgets[widgetId]); } } } diff --git a/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts index 5fdc32e47ee..fbaa5ffcabb 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts @@ -26,8 +26,7 @@ import { HideUnchangedRegionsFeature } from 'vs/editor/browser/widget/diffEditor import { MovedBlocksLinesFeature } from 'vs/editor/browser/widget/diffEditor/features/movedBlocksLinesFeature'; import { OverviewRulerFeature } from 'vs/editor/browser/widget/diffEditor/features/overviewRulerFeature'; import { RevertButtonsFeature } from 'vs/editor/browser/widget/diffEditor/features/revertButtonsFeature'; -import { CSSStyle, ObservableElementSizeObserver, applyStyle, applyViewZones, readHotReloadableExport, translatePosition } from 'vs/editor/browser/widget/diffEditor/utils'; -import { bindContextKey } from 'vs/platform/observable/common/platformObservableUtils'; +import { CSSStyle, ObservableElementSizeObserver, applyStyle, applyViewZones, bindContextKey, readHotReloadableExport, translatePosition } from 'vs/editor/browser/widget/diffEditor/utils'; import { IDiffEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IDimension } from 'vs/editor/common/core/dimension'; import { Position } from 'vs/editor/common/core/position'; diff --git a/src/vs/editor/browser/widget/diffEditor/utils.ts b/src/vs/editor/browser/widget/diffEditor/utils.ts index 3b968353291..65705cb8097 100644 --- a/src/vs/editor/browser/widget/diffEditor/utils.ts +++ b/src/vs/editor/browser/widget/diffEditor/utils.ts @@ -8,7 +8,7 @@ import { findLast } from 'vs/base/common/arraysFind'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { isHotReloadEnabled, registerHotReloadHandler } from 'vs/base/common/hotReload'; import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { IObservable, IReader, ISettableObservable, autorun, autorunHandleChanges, autorunOpts, autorunWithStore, observableSignalFromEvent, observableValue, transaction } from 'vs/base/common/observable'; +import { IObservable, IReader, ISettableObservable, autorun, autorunHandleChanges, autorunOpts, autorunWithStore, observableFromEvent, observableSignalFromEvent, observableValue, transaction } from 'vs/base/common/observable'; import { ElementSizeObserver } from 'vs/editor/browser/config/elementSizeObserver'; import { ICodeEditor, IOverlayWidget, IViewZone } from 'vs/editor/browser/editorBrowser'; import { Position } from 'vs/editor/common/core/position'; @@ -16,6 +16,8 @@ import { Range } from 'vs/editor/common/core/range'; import { DetailedLineRangeMapping } from 'vs/editor/common/diff/rangeMapping'; import { IModelDeltaDecoration } from 'vs/editor/common/model'; import { TextLength } from 'vs/editor/common/core/textLength'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ContextKeyValue, RawContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; export function joinCombine(arr1: readonly T[], arr2: readonly T[], keySelector: (val: T) => number, combine: (v1: T, v2: T) => T): readonly T[] { if (arr1.length === 0) { @@ -87,6 +89,17 @@ export function prependRemoveOnDispose(parent: HTMLElement, child: HTMLElement) }); } +export function observableConfigValue(key: string, defaultValue: T, configurationService: IConfigurationService): IObservable { + return observableFromEvent( + (handleChange) => configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(key)) { + handleChange(e); + } + }), + () => configurationService.getValue(key) ?? defaultValue, + ); +} + export class ObservableElementSizeObserver extends Disposable { private readonly elementSizeObserver: ElementSizeObserver; @@ -427,6 +440,13 @@ function lengthBetweenPositions(position1: Position, position2: Position): TextL } } +export function bindContextKey(key: RawContextKey, service: IContextKeyService, computeValue: (reader: IReader) => T): IDisposable { + const boundKey = key.bindTo(service); + return autorunOpts({ debugName: () => `Set Context Key "${key.key}"` }, reader => { + boundKey.set(computeValue(reader)); + }); +} + export function filterWithPrevious(arr: T[], filter: (cur: T, prev: T | undefined) => boolean): T[] { let prev: T | undefined; return arr.filter(cur => { diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css b/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css index 3bc52c6c915..8afc9c241cf 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css @@ -64,7 +64,6 @@ box-shadow: var(--vscode-editorStickyScroll-shadow) 0 3px 2px -2px; z-index: 4; background-color: var(--vscode-editorStickyScroll-background); - right: initial !important; } .monaco-editor .sticky-widget.peek { diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts index d0e8da4b17a..bdcaafb4891 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts @@ -9,7 +9,7 @@ import { equals } from 'vs/base/common/arrays'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ThemeIcon } from 'vs/base/common/themables'; import 'vs/css!./stickyScroll'; -import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from 'vs/editor/browser/editorBrowser'; import { getColumnOfNodeOffset } from 'vs/editor/browser/viewParts/lines/viewLine'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; import { EditorLayoutInfo, EditorOption, RenderLineNumbersType } from 'vs/editor/common/config/editorOptions'; @@ -387,8 +387,7 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { getPosition(): IOverlayWidgetPosition | null { return { - preference: OverlayWidgetPositionPreference.TOP_CENTER, - stackOridinal: 10, + preference: null }; } diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index b9c7cbd73ab..e2c5bd2ea0b 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -5387,21 +5387,12 @@ declare namespace monaco.editor { * The position preference for the overlay widget. */ preference: OverlayWidgetPositionPreference | IOverlayWidgetPositionCoordinates | null; - /** - * When set, stacks with other overlay widgets with the same preference, - * in an order determined by the ordinal value. - */ - stackOridinal?: number; } /** * An overlay widgets renders on top of the text. */ export interface IOverlayWidget { - /** - * Event fired when the widget layout changes. - */ - onDidLayout?: IEvent; /** * Render this overlay widget in a location where it could overflow the editor's view dom node. */ diff --git a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts index 3ef15eb1b3a..ba277dbc24e 100644 --- a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts +++ b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts @@ -8,13 +8,12 @@ import { getStructuralKey } from 'vs/base/common/equals'; import { Event, IValueWithChangeEvent } from 'vs/base/common/event'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { FileAccess } from 'vs/base/common/network'; -import { derived, observableFromEvent } from 'vs/base/common/observable'; +import { derived, IObservable, observableFromEvent } from 'vs/base/common/observable'; import { ValueWithChangeEventFromObservable } from 'vs/base/common/observableInternal/utils'; import { localize } from 'vs/nls'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; export const IAccessibilitySignalService = createDecorator('accessibilitySignalService'); @@ -202,7 +201,7 @@ export class AccessibilitySignalService extends Disposable implements IAccessibi private readonly _signalConfigValue = new CachedFunction((signal: AccessibilitySignal) => observableConfigValue<{ sound: EnabledState; announcement: EnabledState; - }>(signal.settingsKey, { sound: 'off', announcement: 'off' }, this.configurationService)); + }>(signal.settingsKey, this.configurationService)); private readonly _signalEnabledState = new CachedFunction( { getCacheKey: getStructuralKey }, @@ -590,3 +589,13 @@ export class AccessibilitySignal { }); } +export function observableConfigValue(key: string, configurationService: IConfigurationService): IObservable { + return observableFromEvent( + (handleChange) => configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(key)) { + handleChange(e); + } + }), + () => configurationService.getValue(key), + ); +} diff --git a/src/vs/platform/observable/common/platformObservableUtils.ts b/src/vs/platform/observable/common/platformObservableUtils.ts deleted file mode 100644 index 096993beb80..00000000000 --- a/src/vs/platform/observable/common/platformObservableUtils.ts +++ /dev/null @@ -1,30 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IDisposable } from 'vs/base/common/lifecycle'; -import { autorunOpts, IObservable, IReader, observableFromEvent } from 'vs/base/common/observable'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ContextKeyValue, RawContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; - -/** Creates an observable update when a configuration key updates. */ -export function observableConfigValue(key: string, defaultValue: T, configurationService: IConfigurationService): IObservable { - return observableFromEvent( - (handleChange) => configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(key)) { - handleChange(e); - } - }), - () => configurationService.getValue(key) ?? defaultValue - ); -} - -/** Update the configuration key with a value derived from observables. */ -export function bindContextKey(key: RawContextKey, service: IContextKeyService, computeValue: (reader: IReader) => T): IDisposable { - const boundKey = key.bindTo(service); - return autorunOpts({ debugName: () => `Set Context Key "${key.key}"` }, reader => { - boundKey.set(computeValue(reader)); - }); -} - diff --git a/src/vs/workbench/contrib/mergeEditor/browser/model/diffComputer.ts b/src/vs/workbench/contrib/mergeEditor/browser/model/diffComputer.ts index 56760afd5eb..278615a60b2 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/model/diffComputer.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/model/diffComputer.ts @@ -11,7 +11,7 @@ import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model/lineRange'; import { DetailedLineRangeMapping, RangeMapping } from 'vs/workbench/contrib/mergeEditor/browser/model/mapping'; -import { observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; +import { observableConfigValue } from 'vs/workbench/contrib/mergeEditor/browser/utils'; import { LineRange as DiffLineRange } from 'vs/editor/common/core/lineRange'; export interface IMergeDiffComputer { diff --git a/src/vs/workbench/contrib/mergeEditor/browser/utils.ts b/src/vs/workbench/contrib/mergeEditor/browser/utils.ts index 5ac6522a14b..c085272472c 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/utils.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/utils.ts @@ -6,9 +6,10 @@ import { ArrayQueue, CompareResult } from 'vs/base/common/arrays'; import { onUnexpectedError } from 'vs/base/common/errors'; import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { IObservable, autorunOpts } from 'vs/base/common/observable'; +import { IObservable, autorunOpts, observableFromEvent } from 'vs/base/common/observable'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { IModelDeltaDecoration } from 'vs/editor/common/model'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; export function setStyle( @@ -155,3 +156,13 @@ export class PersistentStore { } } +export function observableConfigValue(key: string, defaultValue: T, configurationService: IConfigurationService): IObservable { + return observableFromEvent( + (handleChange) => configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(key)) { + handleChange(e); + } + }), + () => configurationService.getValue(key) ?? defaultValue, + ); +} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts index 29af08fbafe..8cd243e3777 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts @@ -20,8 +20,7 @@ import { MenuId } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { DEFAULT_EDITOR_MAX_DIMENSIONS, DEFAULT_EDITOR_MIN_DIMENSIONS } from 'vs/workbench/browser/parts/editor/editor'; -import { setStyle } from 'vs/workbench/contrib/mergeEditor/browser/utils'; -import { observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; +import { observableConfigValue, setStyle } from 'vs/workbench/contrib/mergeEditor/browser/utils'; import { MergeEditorViewModel } from 'vs/workbench/contrib/mergeEditor/browser/view/viewModel'; export abstract class CodeEditorView extends Disposable { diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts index bec1dec9481..3609b0046fa 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts @@ -39,8 +39,7 @@ import { readTransientState, writeTransientState } from 'vs/workbench/contrib/co import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; import { IMergeEditorInputModel } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInputModel'; import { MergeEditorModel } from 'vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel'; -import { deepMerge, PersistentStore, thenIfNotDisposed } from 'vs/workbench/contrib/mergeEditor/browser/utils'; -import { observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; +import { deepMerge, observableConfigValue, PersistentStore, thenIfNotDisposed } from 'vs/workbench/contrib/mergeEditor/browser/utils'; import { BaseCodeEditorView } from 'vs/workbench/contrib/mergeEditor/browser/view/editors/baseCodeEditorView'; import { ScrollSynchronizer } from 'vs/workbench/contrib/mergeEditor/browser/view/scrollSynchronizer'; import { MergeEditorViewModel } from 'vs/workbench/contrib/mergeEditor/browser/view/viewModel'; diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts index 1c0f093dc27..ca8a00e6b56 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts @@ -15,7 +15,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model/lineRange'; import { MergeEditorModel } from 'vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel'; import { InputNumber, ModifiedBaseRange, ModifiedBaseRangeState } from 'vs/workbench/contrib/mergeEditor/browser/model/modifiedBaseRange'; -import { observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; +import { observableConfigValue } from 'vs/workbench/contrib/mergeEditor/browser/utils'; import { BaseCodeEditorView } from 'vs/workbench/contrib/mergeEditor/browser/view/editors/baseCodeEditorView'; import { CodeEditorView } from 'vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView'; import { InputCodeEditorView } from 'vs/workbench/contrib/mergeEditor/browser/view/editors/inputCodeEditorView'; diff --git a/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts b/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts index 9040875421a..a6ce0ce1d1c 100644 --- a/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts +++ b/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts @@ -4,20 +4,16 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; -import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; -import { ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; -import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; -import { Action } from 'vs/base/common/actions'; import { mapFindFirst } from 'vs/base/common/arraysFind'; import { assert, assertNever } from 'vs/base/common/assert'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Lazy } from 'vs/base/common/lazy'; -import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { autorun, derived, observableFromEvent, observableValue } from 'vs/base/common/observable'; import { ThemeIcon } from 'vs/base/common/themables'; -import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, MouseTargetType, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; @@ -25,24 +21,20 @@ import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { IModelDecorationOptions, InjectedTextCursorStops, InjectedTextOptions, ITextModel } from 'vs/editor/common/model'; import { localize, localize2 } from 'vs/nls'; import { Categories } from 'vs/platform/action/common/actionCommonCategories'; -import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ILogService } from 'vs/platform/log/common/log'; -import { observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; import { IQuickInputService, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; import * as coverUtils from 'vs/workbench/contrib/testing/browser/codeCoverageDisplayUtils'; -import { testingCoverageIcon, testingCoverageMissingBranch, testingFilterIcon, testingRerunIcon } from 'vs/workbench/contrib/testing/browser/icons'; +import { testingCoverageMissingBranch } from 'vs/workbench/contrib/testing/browser/icons'; import { ManagedTestCoverageBars } from 'vs/workbench/contrib/testing/browser/testCoverageBars'; import { getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration'; -import { TestCommandId } from 'vs/workbench/contrib/testing/common/constants'; import { FileCoverage } from 'vs/workbench/contrib/testing/common/testCoverage'; import { ITestCoverageService } from 'vs/workbench/contrib/testing/common/testCoverageService'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; -import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; import { CoverageDetails, DetailType, IDeclarationCoverage, IStatementCoverage } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; @@ -60,7 +52,7 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri private loadingCancellation?: CancellationTokenSource; private readonly displayedStore = this._register(new DisposableStore()); private readonly hoveredStore = this._register(new DisposableStore()); - private readonly summaryWidget: Lazy; + private readonly summaryWidget: Lazy; private decorationIds = new Map this._register(instantiationService.createInstance(CoverageToolbarWidget, this.editor))); + this.summaryWidget = new Lazy(() => this._register(instantiationService.createInstance(CoverageSummaryWidget, this.editor))); const modelObs = observableFromEvent(editor.onDidChangeModel, () => editor.getModel()); const configObs = observableFromEvent(editor.onDidChangeConfiguration, i => i); @@ -117,16 +108,6 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri } })); - const toolbarEnabled = observableConfigValue(TestingConfigKeys.CoverageToolbarEnabled, true, configurationService); - this._register(autorun(reader => { - const c = fileCoverage.read(reader); - if (c && toolbarEnabled.read(reader)) { - this.summaryWidget.value.setCoverage(c); - } else { - this.summaryWidget.rawValue?.setCoverage(undefined); - } - })); - this._register(autorun(reader => { const c = fileCoverage.read(reader); if (c) { @@ -264,6 +245,7 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri } this.displayedStore.clear(); + this.summaryWidget.value.setCoverage(coverage); model.changeDecorations(e => { for (const detailRange of details.ranges) { @@ -327,6 +309,8 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri }); this.displayedStore.add(toDisposable(() => { + this.summaryWidget.value.setCoverage(undefined); + model.changeDecorations(e => { for (const decoration of this.decorationIds.keys()) { e.removeDecoration(decoration); @@ -529,81 +513,42 @@ function wrapName(functionNameOrCode: string) { return wrapInBackticks(functionNameOrCode); } -class CoverageToolbarWidget extends Disposable implements IOverlayWidget { +class CoverageSummaryWidget implements IDisposable { private current: FileCoverage | undefined; private registered = false; - private isRunning = false; - private readonly showStore = this._register(new DisposableStore()); - private readonly actionBar: ActionBar; + private readonly registration = new DisposableStore(); + private readonly _domNode = dom.h('div.coverage-summary-widget', [ dom.h('div', [ dom.h('span.bars@bars'), dom.h('span.stat@stat'), - dom.h('span.toolbar@toolbar'), + dom.h('a.toggleInline@toggleInline'), + dom.h('a.perTestFilter@perTestFilter'), ]), ]); private readonly bars: ManagedTestCoverageBars; + constructor( private readonly editor: ICodeEditor, @IConfigurationService private readonly configurationService: IConfigurationService, @IQuickInputService private readonly quickInputService: IQuickInputService, @ITestCoverageService private readonly testCoverageService: ITestCoverageService, - @IContextMenuService private readonly contextMenuService: IContextMenuService, - @ITestService private readonly testService: ITestService, - @IKeybindingService private readonly keybindingService: IKeybindingService, + @IKeybindingService keybindingService: IKeybindingService, @IInstantiationService instaService: IInstantiationService, ) { - super(); - - this.bars = this._register(instaService.createInstance(ManagedTestCoverageBars, { + this._domNode.perTestFilter.ariaLabel = this._domNode.perTestFilter.title = coverUtils.labels.clickToChangeFiltering; + this.bars = instaService.createInstance(ManagedTestCoverageBars, { compact: false, overall: false, container: this._domNode.bars, - })); + }); - this.actionBar = this._register(instaService.createInstance(ActionBar, this._domNode.toolbar, { - orientation: ActionsOrientation.HORIZONTAL, - actionViewItemProvider: (action, options) => { - const vm = new CodiconActionViewItem(undefined, action, options); - if (action instanceof ActionWithIcon) { - vm.themeIcon = action.icon; - } - return vm; - } - })); - - - this._register(autorun(reader => { - CodeCoverageDecorations.showInline.read(reader); - this.setActions(); - })); - - this._register(dom.addStandardDisposableListener(this._domNode.root, dom.EventType.CONTEXT_MENU, e => { - this.contextMenuService.showContextMenu({ - menuId: MenuId.StickyScrollContext, - getAnchor: () => e, - }); - })); - } - - /** @inheritdoc */ - public getId(): string { - return 'coverage-summary-widget'; - } - - /** @inheritdoc */ - public getDomNode(): HTMLElement { - return this._domNode.root; - } - - /** @inheritdoc */ - public getPosition(): IOverlayWidgetPosition | null { - return { - preference: OverlayWidgetPositionPreference.TOP_CENTER, - stackOridinal: 9, - }; + const kb = keybindingService.lookupKeybinding(TOGGLE_INLINE_COMMAND_ID); + if (kb) { + this._domNode.toggleInline.title = `${TOGGLE_INLINE_COMMAND_TEXT} (${kb.getLabel()})`; + } } public setCoverage(coverage: FileCoverage | undefined) { @@ -611,13 +556,32 @@ class CoverageToolbarWidget extends Disposable implements IOverlayWidget { this.bars.setCoverageInfo(coverage); if (!coverage) { - return this.hide(); + return this.unregister(); } const displayStat = coverUtils.calculateDisplayedStat(coverage, getTestingConfiguration(this.configurationService, TestingConfigKeys.CoveragePercent)); this._domNode.stat.innerText = localize('testing.percentCoverage', '{0} Coverage', coverUtils.displayPercent(displayStat)); - this.setActions(); - this.show(); + + this._domNode.perTestFilter.classList.toggle('active', !!coverage.isForTest); + if (coverage.isForTest) { + const testItem = coverage.fromResult.getTestById(coverage.isForTest.id.toString()); + assert(!!testItem, 'got coverage for an unreported test'); + this._domNode.perTestFilter.style.display = 'inline'; + this._domNode.perTestFilter.innerText = coverUtils.labels.showingFilterFor(testItem.label); + } else if (coverage.perTestData?.size) { + this._domNode.perTestFilter.style.display = 'inline'; + this._domNode.perTestFilter.innerText = localize('testing.coverageForTestAvailable', "{0} test(s) in this file", coverage.perTestData.size); + } else { + this._domNode.perTestFilter.style.display = 'none'; + } + + this.register(); + } + + /** @inheritdoc */ + public dispose() { + this.unregister(); + this.bars.dispose(); } private filterTest() { @@ -639,144 +603,65 @@ class CoverageToolbarWidget extends Disposable implements IOverlayWidget { ...tests.map(item => ({ label: coverUtils.getLabelForItem(result, item.isForTest!.id, commonPrefix), description: coverUtils.labels.percentCoverage(item.tpc), item })), ]; - // These handle the behavior that reveals the start of coverage when the - // user picks from the quickpick. Scroll position is restored if the user - // exits without picking an item, or picks "all tets". - const scrollTop = this.editor.getScrollTop(); - const revealScrollCts = new MutableDisposable(); - this.quickInputService.pick(items, { activeItem: items.find((item): item is TItem => 'item' in item && item.item === this.current), placeHolder: coverUtils.labels.pickShowCoverage, onDidFocus: (entry) => { - if (!entry.item) { - revealScrollCts.clear(); - this.editor.setScrollTop(scrollTop); - this.testCoverageService.filterToTest.set(undefined, undefined); - } else { - const cts = revealScrollCts.value = new CancellationTokenSource(); - entry.item.details(cts.token).then( - details => { - const first = details.find(d => d.type === DetailType.Statement); - if (!cts.token.isCancellationRequested && first) { - this.editor.revealLineNearTop(first.location instanceof Position ? first.location.lineNumber : first.location.startLineNumber); - } - }, - () => { /* ignored */ } - ); - this.testCoverageService.filterToTest.set(entry.item.isForTest!.id, undefined); - } + this.testCoverageService.filterToTest.set(entry.item?.isForTest!.id, undefined); }, }).then(selected => { - if (!selected) { - this.editor.setScrollTop(scrollTop); - } - - revealScrollCts.dispose(); this.testCoverageService.filterToTest.set(selected ? selected.item?.isForTest!.id : previousSelection, undefined); }); } - private setActions() { - this.actionBar.clear(); - const coverage = this.current; - if (!coverage) { - return; - } - - const toggleAction = new ActionWithIcon( - 'toggleInline', - CodeCoverageDecorations.showInline.get() - ? localize('testing.hideInlineCoverage', 'Hide Inline Coverage') - : localize('testing.showInlineCoverage', 'Show Inline Coverage'), - testingCoverageIcon, - undefined, - () => CodeCoverageDecorations.showInline.set(!CodeCoverageDecorations.showInline.get(), undefined), - ); - - const kb = this.keybindingService.lookupKeybinding(TOGGLE_INLINE_COMMAND_ID); - if (kb) { - toggleAction.tooltip = `${TOGGLE_INLINE_COMMAND_TEXT} (${kb.getLabel()})`; - } - - this.actionBar.push(toggleAction); - - if (coverage.isForTest) { - const testItem = coverage.fromResult.getTestById(coverage.isForTest.id.toString()); - assert(!!testItem, 'got coverage for an unreported test'); - this.actionBar.push(new ActionWithIcon('perTestFilter', - coverUtils.labels.showingFilterFor(testItem.label), - testingFilterIcon, - undefined, - () => this.filterTest(), - )); - } else if (coverage.perTestData?.size) { - this.actionBar.push(new ActionWithIcon('perTestFilter', - localize('testing.coverageForTestAvailable', "{0} test(s) in this file", coverage.perTestData.size), - testingFilterIcon, - undefined, - () => this.filterTest(), - )); - } - - this.actionBar.push(new ActionWithIcon( - 'rerun', - localize('testing.rerun', 'Rerun'), - testingRerunIcon, - !this.isRunning, - () => this.rerunTest() - )); - } - - private show() { + private register() { if (this.registered) { return; } this.registered = true; - let viewZoneId: string; - const ds = this.showStore; - this.editor.addOverlayWidget(this); + let viewZoneId: string; this.editor.changeViewZones(accessor => { - viewZoneId = accessor.addZone({ // make space for the widget + viewZoneId = accessor.addZone({ afterLineNumber: 0, afterColumn: 0, - domNode: document.createElement('div'), + domNode: this._domNode.root, heightInPx: 30, ordinal: -1, // show before code lenses }); }); - ds.add(toDisposable(() => { - this.registered = false; - this.editor.removeOverlayWidget(this); + this.registration.add(toDisposable(() => { this.editor.changeViewZones(accessor => { accessor.removeZone(viewZoneId); }); + this.registered = false; })); - ds.add(this.configurationService.onDidChangeConfiguration(e => { + this.registration.add(dom.addStandardDisposableListener(this._domNode.perTestFilter, 'click', () => { + this.filterTest(); + })); + + this.registration.add(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(TestingConfigKeys.CoverageBarThresholds) || e.affectsConfiguration(TestingConfigKeys.CoveragePercent)) { this.setCoverage(this.current); } })); + + this.registration.add(dom.addStandardDisposableListener(this._domNode.toggleInline, 'click', () => { + CodeCoverageDecorations.showInline.set(!CodeCoverageDecorations.showInline.get(), undefined); + })); + + this.registration.add(autorun(reader => { + this._domNode.toggleInline.innerText = CodeCoverageDecorations.showInline.read(reader) + ? localize('testing.hideInlineCoverage', 'Hide Inline Coverage') + : localize('testing.showInlineCoverage', 'Show Inline Coverage'); + })); } - private rerunTest() { - const current = this.current; - if (current) { - this.isRunning = true; - this.setActions(); - this.testService.runResolvedTests(current.fromResult.request).finally(() => { - this.isRunning = false; - this.setActions(); - }); - } - } - - private hide() { - this.showStore.clear(); + private unregister() { + this.registration.clear(); } } @@ -798,47 +683,3 @@ registerAction2(class ToggleInlineCoverage extends Action2 { CodeCoverageDecorations.showInline.set(!CodeCoverageDecorations.showInline.get(), undefined); } }); - -registerAction2(class ToggleCoverageToolbar extends Action2 { - constructor() { - super({ - id: TestCommandId.CoverageToggleToolbar, - title: localize2('testing.toggleToolbarTitle', "Toggle Coverage Toolbar"), - metadata: { - description: localize2('testing.toggleToolbarDesc', 'Toggle the sticky coverage bar in the editor.') - }, - category: Categories.Test, - toggled: { - condition: TestingContextKeys.coverageToolbarEnabled, - title: localize('cmd.toggle2', "Toggle Coverage Toolbar"), - }, - menu: [ - { id: MenuId.CommandPalette, when: TestingContextKeys.isTestCoverageOpen }, - { id: MenuId.StickyScrollContext, when: TestingContextKeys.isTestCoverageOpen }, - ] - }); - } - - run(accessor: ServicesAccessor): void { - const config = accessor.get(IConfigurationService); - const value = getTestingConfiguration(config, TestingConfigKeys.CoverageToolbarEnabled); - config.updateValue(TestingConfigKeys.CoverageToolbarEnabled, !value); - } -}); - -class ActionWithIcon extends Action { - constructor(id: string, title: string, public readonly icon: ThemeIcon, enabled: boolean | undefined, run: () => void) { - super(id, title, undefined, enabled, run); - } -} - -class CodiconActionViewItem extends ActionViewItem { - - public themeIcon?: ThemeIcon; - - protected override updateLabel(): void { - if (this.options.label && this.label && this.themeIcon) { - dom.reset(this.label, renderIcon(this.themeIcon), this.action.label); - } - } -} diff --git a/src/vs/workbench/contrib/testing/browser/media/testing.css b/src/vs/workbench/contrib/testing/browser/media/testing.css index 3068f3db57f..4271a033ccc 100644 --- a/src/vs/workbench/contrib/testing/browser/media/testing.css +++ b/src/vs/workbench/contrib/testing/browser/media/testing.css @@ -404,50 +404,50 @@ .coverage-summary-widget { color: var(--vscode-editor-foreground); z-index: 1; - background: var(--vscode-editor-background); - left: 0; - width: 100%; - box-shadow: var(--vscode-editorStickyScroll-shadow) 0 3px 2px -2px; + line-height: 25px; > div { display: flex; align-items: center; - padding: 0 22px; - height: 25px; + border-bottom: 1px solid var(--vscode-menu-border); } - .btn { + .toggleInline, .perTestFilter { + border-left: 1px solid var(--vscode-menu-border); + padding: 0 6px; + } + + .stat, .toggleInline { + padding-right: 6px; + } + + > span, > a { + display: inline; position: relative; - margin: 0 4px; - padding: 0 4px; + padding: 0 6px; &:first-child { - margin-left: 0; + padding-left: 0; } &:last-child { - margin-right: 0; + padding-right: 0; } } - .stat, .action-label { - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - margin: 0 3px; + + a { + color: var(--vscode-textLink-foreground); + cursor: pointer; } - .action-label { - display: flex; - align-items: center; - font-size: 13px; - padding: 0 4px; - - .codicon { - margin-right: 4px; - } + a:hover { + color: var(--vscode-textLink-activeForeground); } + .toggleInline, .perTestFilter { + border-left: 1px solid var(--vscode-menu-border); + } } .test-coverage-tree-per-test-switcher { diff --git a/src/vs/workbench/contrib/testing/browser/testCoverageView.ts b/src/vs/workbench/contrib/testing/browser/testCoverageView.ts index 841903f8096..ff7bda28075 100644 --- a/src/vs/workbench/contrib/testing/browser/testCoverageView.ts +++ b/src/vs/workbench/contrib/testing/browser/testCoverageView.ts @@ -23,7 +23,6 @@ import { URI } from 'vs/base/common/uri'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { localize, localize2 } from 'vs/nls'; -import { Categories } from 'vs/platform/action/common/actionCommonCategories'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -686,7 +685,6 @@ registerAction2(class TestCoverageChangePerTestFilterAction extends Action2 { constructor() { super({ id: TestCommandId.CoverageFilterToTest, - category: Categories.Test, title: localize2('testing.changeCoverageFilter', 'Filter Coverage by Test...'), precondition: TestingContextKeys.hasPerTestCoverage, f1: true, diff --git a/src/vs/workbench/contrib/testing/common/configuration.ts b/src/vs/workbench/contrib/testing/common/configuration.ts index ec9e50d67f0..1bbc290e9cd 100644 --- a/src/vs/workbench/contrib/testing/common/configuration.ts +++ b/src/vs/workbench/contrib/testing/common/configuration.ts @@ -23,7 +23,6 @@ export const enum TestingConfigKeys { CoveragePercent = 'testing.displayedCoveragePercent', ShowCoverageInExplorer = 'testing.showCoverageInExplorer', CoverageBarThresholds = 'testing.coverageBarThresholds', - CoverageToolbarEnabled = 'testing.coverageToolbarEnabled', } export const enum AutoOpenTesting { @@ -191,11 +190,6 @@ export const testingConfiguration: IConfigurationNode = { green: { type: 'number', minimum: 0, maximum: 100, default: 90 }, }, }, - [TestingConfigKeys.CoverageToolbarEnabled]: { - description: localize('testing.coverageToolbarEnabled', 'Controls whether the coverage toolbar is shown in the editor.'), - type: 'boolean', - default: false, // todo@connor4312: disabled by default until UI sync - }, } }; @@ -220,7 +214,6 @@ export interface ITestingConfiguration { [TestingConfigKeys.CoveragePercent]: TestingDisplayedCoveragePercent; [TestingConfigKeys.ShowCoverageInExplorer]: boolean; [TestingConfigKeys.CoverageBarThresholds]: ITestingCoverageBarThresholds; - [TestingConfigKeys.CoverageToolbarEnabled]: boolean; } export const getTestingConfiguration = (config: IConfigurationService, key: K) => config.getValue(key); diff --git a/src/vs/workbench/contrib/testing/common/constants.ts b/src/vs/workbench/contrib/testing/common/constants.ts index e879003b6a1..2dfd8cf55c2 100644 --- a/src/vs/workbench/contrib/testing/common/constants.ts +++ b/src/vs/workbench/contrib/testing/common/constants.ts @@ -68,7 +68,6 @@ export const enum TestCommandId { CoverageFilterToTest = 'testing.coverageFilterToTest', CoverageLastRun = 'testing.coverageLastRun', CoverageSelectedAction = 'testing.coverageSelected', - CoverageToggleToolbar = 'testing.coverageToggleToolbar', CoverageViewChangeSorting = 'testing.coverageViewChangeSorting', DebugAction = 'testing.debug', DebugAllAction = 'testing.debugAll', diff --git a/src/vs/workbench/contrib/testing/common/testCoverageService.ts b/src/vs/workbench/contrib/testing/common/testCoverageService.ts index 99e86e86a95..1336f748f86 100644 --- a/src/vs/workbench/contrib/testing/common/testCoverageService.ts +++ b/src/vs/workbench/contrib/testing/common/testCoverageService.ts @@ -6,11 +6,8 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { IObservable, ISettableObservable, observableValue, transaction } from 'vs/base/common/observable'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { bindContextKey, observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; -import { TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration'; import { Testing } from 'vs/workbench/contrib/testing/common/constants'; import { TestCoverage } from 'vs/workbench/contrib/testing/common/testCoverage'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; @@ -48,6 +45,8 @@ export interface ITestCoverageService { export class TestCoverageService extends Disposable implements ITestCoverageService { declare readonly _serviceBrand: undefined; + private readonly _isOpenKey: IContextKey; + private readonly _hasPerTestCoverage: IContextKey; private readonly lastOpenCts = this._register(new MutableDisposable()); public readonly selected = observableValue('testCoverage', undefined); @@ -56,29 +55,11 @@ export class TestCoverageService extends Disposable implements ITestCoverageServ constructor( @IContextKeyService contextKeyService: IContextKeyService, @ITestResultService resultService: ITestResultService, - @IConfigurationService configService: IConfigurationService, @IViewsService private readonly viewsService: IViewsService, ) { super(); - - const toolbarConfig = observableConfigValue(TestingConfigKeys.CoverageToolbarEnabled, true, configService); - this._register(bindContextKey( - TestingContextKeys.coverageToolbarEnabled, - contextKeyService, - reader => toolbarConfig.read(reader), - )); - - this._register(bindContextKey( - TestingContextKeys.isTestCoverageOpen, - contextKeyService, - reader => !!this.selected.read(reader), - )); - - this._register(bindContextKey( - TestingContextKeys.hasPerTestCoverage, - contextKeyService, - reader => !!this.selected.read(reader)?.perTestCoverageIDs.size, - )); + this._isOpenKey = TestingContextKeys.isTestCoverageOpen.bindTo(contextKeyService); + this._hasPerTestCoverage = TestingContextKeys.hasPerTestCoverage.bindTo(contextKeyService); this._register(resultService.onResultsChanged(evt => { if ('completed' in evt) { @@ -111,6 +92,8 @@ export class TestCoverageService extends Disposable implements ITestCoverageServ this.filterToTest.set(undefined, tx); this.selected.set(coverage, tx); }); + this._isOpenKey.set(true); + this._hasPerTestCoverage.set(coverage.perTestCoverageIDs.size > 0); if (focus && !cts.token.isCancellationRequested) { this.viewsService.openView(Testing.CoverageViewId, true); @@ -119,6 +102,8 @@ export class TestCoverageService extends Disposable implements ITestCoverageServ /** @inheritdoc */ public closeCoverage() { + this._isOpenKey.set(false); + this._hasPerTestCoverage.set(false); this.selected.set(undefined, undefined); } } diff --git a/src/vs/workbench/contrib/testing/common/testingContextKeys.ts b/src/vs/workbench/contrib/testing/common/testingContextKeys.ts index 2c3d0b8c79f..7878be0ec9e 100644 --- a/src/vs/workbench/contrib/testing/common/testingContextKeys.ts +++ b/src/vs/workbench/contrib/testing/common/testingContextKeys.ts @@ -23,7 +23,6 @@ export namespace TestingContextKeys { export const activeEditorHasTests = new RawContextKey('testing.activeEditorHasTests', false, { type: 'boolean', description: localize('testing.activeEditorHasTests', 'Indicates whether any tests are present in the current editor') }); export const isTestCoverageOpen = new RawContextKey('testing.isTestCoverageOpen', false, { type: 'boolean', description: localize('testing.isTestCoverageOpen', 'Indicates whether a test coverage report is open') }); export const hasPerTestCoverage = new RawContextKey('testing.hasPerTestCoverage', false, { type: 'boolean', description: localize('testing.hasPerTestCoverage', 'Indicates whether per-test coverage is available') }); - export const coverageToolbarEnabled = new RawContextKey('testing.coverageToolbarEnabled', true, { type: 'boolean', description: localize('testing.coverageToolbarEnabled', 'Indicates whether the coverage toolbar is enabled') }); export const capabilityToContextKey: { [K in TestRunProfileBitset]: RawContextKey } = { [TestRunProfileBitset.Run]: hasRunnableTests, diff --git a/src/vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController.ts b/src/vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController.ts index 3695379f0e9..850b58e1e6c 100644 --- a/src/vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController.ts +++ b/src/vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController.ts @@ -5,7 +5,7 @@ import { importAMDNodeModule } from 'vs/amdX'; import { Disposable } from 'vs/base/common/lifecycle'; -import { IObservable, autorun, keepObserved } from 'vs/base/common/observable'; +import { IObservable, autorun, keepObserved, observableFromEvent } from 'vs/base/common/observable'; import { countEOL } from 'vs/editor/common/core/eolCounter'; import { LineRange } from 'vs/editor/common/core/lineRange'; import { Range } from 'vs/editor/common/core/range'; @@ -15,7 +15,6 @@ import { TokenizationStateStore } from 'vs/editor/common/model/textModelTokens'; import { IModelContentChange, IModelContentChangedEvent } from 'vs/editor/common/textModelEvents'; import { ContiguousMultilineTokensBuilder } from 'vs/editor/common/tokens/contiguousMultilineTokensBuilder'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; import { ArrayEdit, MonotonousIndexTransformer, SingleArrayEdit } from 'vs/workbench/services/textMate/browser/arrayOperation'; import type { StateDeltas, TextMateTokenizationWorker } from 'vs/workbench/services/textMate/browser/backgroundTokenization/worker/textMateTokenizationWorker.worker'; import type { applyStateStackDiff, StateStack } from 'vscode-textmate'; @@ -238,3 +237,13 @@ function changesToString(changes: IModelContentChange[]): string { return changes.map(c => Range.lift(c.range).toString() + ' => ' + c.text).join(' & '); } +function observableConfigValue(key: string, defaultValue: T, configurationService: IConfigurationService): IObservable { + return observableFromEvent( + (handleChange) => configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(key)) { + handleChange(e); + } + }), + () => configurationService.getValue(key) ?? defaultValue, + ); +} From 2041cd46821d82590c5ca76da5e761db38e23097 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 13 May 2024 12:54:33 +0200 Subject: [PATCH 123/357] fix #212145 (#212595) --- src/vs/platform/userDataSync/common/extensionsSync.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/platform/userDataSync/common/extensionsSync.ts b/src/vs/platform/userDataSync/common/extensionsSync.ts index be0c0e0bbb4..a490da8344a 100644 --- a/src/vs/platform/userDataSync/common/extensionsSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsSync.ts @@ -483,6 +483,7 @@ export class LocalExtensionsProvider { isMachineScoped: false /* set isMachineScoped value to prevent install and sync dialog in web */, donotIncludePackAndDependencies: true, installGivenVersion: e.pinned && !!e.version, + pinned: e.pinned, installPreReleaseVersion: e.preRelease, profileLocation: profile.extensionsResource, isApplicationScoped: e.isApplicationScoped, From 3028408922e78cc9ca15ee761372a7c0603fa375 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 13 May 2024 12:56:33 +0200 Subject: [PATCH 124/357] don't depend on `ITextModel` from line range util (#212596) --- src/vs/editor/common/core/lineRange.ts | 27 ------------------- .../inlineChat/browser/inlineChatSession.ts | 10 +++++-- 2 files changed, 8 insertions(+), 29 deletions(-) diff --git a/src/vs/editor/common/core/lineRange.ts b/src/vs/editor/common/core/lineRange.ts index cd5610f41e8..1a7a0d6b527 100644 --- a/src/vs/editor/common/core/lineRange.ts +++ b/src/vs/editor/common/core/lineRange.ts @@ -7,7 +7,6 @@ import { BugIndicatingError } from 'vs/base/common/errors'; import { OffsetRange } from 'vs/editor/common/core/offsetRange'; import { Range } from 'vs/editor/common/core/range'; import { findFirstIdxMonotonousOrArrLen, findLastIdxMonotonous, findLastMonotonous } from 'vs/base/common/arraysFind'; -import { ITextModel } from 'vs/editor/common/model'; /** * A range of lines (1-based). @@ -77,32 +76,6 @@ export class LineRange { return new LineRange(lineRange[0], lineRange[1]); } - /** - * @internal - */ - public static invert(range: LineRange, model: ITextModel): LineRange[] { - if (range.isEmpty) { - return []; - } - const result: LineRange[] = []; - if (range.startLineNumber > 1) { - result.push(new LineRange(1, range.startLineNumber)); - } - if (range.endLineNumberExclusive < model.getLineCount() + 1) { - result.push(new LineRange(range.endLineNumberExclusive, model.getLineCount() + 1)); - } - return result.filter(r => !r.isEmpty); - } - - /** - * @internal - */ - public static asRange(lineRange: LineRange, model: ITextModel): Range { - return lineRange.isEmpty - ? new Range(lineRange.startLineNumber, 1, lineRange.startLineNumber, model.getLineLength(lineRange.startLineNumber)) - : new Range(lineRange.startLineNumber, 1, lineRange.endLineNumberExclusive - 1, model.getLineLength(lineRange.endLineNumberExclusive - 1)); - } - /** * The start line number. */ diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts index fb03e5a7f08..f0e890ebfae 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts @@ -450,6 +450,12 @@ export class StashedSession { // --- +function lineRangeAsRange(lineRange: LineRange, model: ITextModel): Range { + return lineRange.isEmpty + ? new Range(lineRange.startLineNumber, 1, lineRange.startLineNumber, model.getLineLength(lineRange.startLineNumber)) + : new Range(lineRange.startLineNumber, 1, lineRange.endLineNumberExclusive - 1, model.getLineLength(lineRange.endLineNumberExclusive - 1)); +} + export class HunkData { private static readonly _HUNK_TRACKED_RANGE = ModelDecorationOptions.register({ @@ -636,8 +642,8 @@ export class HunkData { const textModelNDecorations: string[] = []; const textModel0Decorations: string[] = []; - textModelNDecorations.push(accessorN.addDecoration(LineRange.asRange(hunk.modified, this._textModelN), HunkData._HUNK_TRACKED_RANGE)); - textModel0Decorations.push(accessor0.addDecoration(LineRange.asRange(hunk.original, this._textModel0), HunkData._HUNK_TRACKED_RANGE)); + textModelNDecorations.push(accessorN.addDecoration(lineRangeAsRange(hunk.modified, this._textModelN), HunkData._HUNK_TRACKED_RANGE)); + textModel0Decorations.push(accessor0.addDecoration(lineRangeAsRange(hunk.original, this._textModel0), HunkData._HUNK_TRACKED_RANGE)); for (const change of hunk.changes) { textModelNDecorations.push(accessorN.addDecoration(change.modifiedRange, HunkData._HUNK_TRACKED_RANGE)); From dd4f436c64b915f322f1a3f006337078c979b9f3 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Mon, 13 May 2024 14:16:54 +0200 Subject: [PATCH 125/357] Adding tokenization support to the `autoindent.test.ts` file (#212594) adding tokenization support to the auto indentation test cases --- .../codeEditor/test/node/autoindent.test.ts | 85 ++++++++++++++++++- 1 file changed, 83 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts b/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts index cc9316468bc..9344f1a46a6 100644 --- a/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts +++ b/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts @@ -18,6 +18,10 @@ import { IRange } from 'vs/editor/common/core/range'; import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { trimTrailingWhitespace } from 'vs/editor/common/commands/trimTrailingWhitespaceCommand'; import { execSync } from 'child_process'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { EncodedTokenizationResult, IState, ITokenizationSupport, TokenizationRegistry } from 'vs/editor/common/languages'; +import { NullState } from 'vs/editor/common/languages/nullTokenize'; +import { MetadataConsts, StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; function getIRange(range: IRange): IRange { return { @@ -32,7 +36,12 @@ const enum LanguageId { TypeScript = 'ts-test' } -function registerLanguage(languageConfigurationService: ILanguageConfigurationService, languageId: LanguageId): IDisposable { +function registerLanguage(instantiationService: TestInstantiationService, languageId: LanguageId): IDisposable { + const languageService = instantiationService.get(ILanguageService) + return languageService.registerLanguage({ id: languageId }); +} + +function registerLanguageConfiguration(languageConfigurationService: ILanguageConfigurationService, languageId: LanguageId): IDisposable { let configPath: string; switch (languageId) { case LanguageId.TypeScript: @@ -47,6 +56,33 @@ function registerLanguage(languageConfigurationService: ILanguageConfigurationSe return languageConfigurationService.register(languageId, languageConfig); } +interface StandardTokenTypeData { + startIndex: number; + standardTokenType: StandardTokenType; +} + +function registerTokenizationSupport(instantiationService: TestInstantiationService, tokens: StandardTokenTypeData[][], languageId: string): IDisposable { + 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].standardTokenType << MetadataConsts.TOKEN_TYPE_OFFSET)); + } + return new EncodedTokenizationResult(result, state); + } + }; + return TokenizationRegistry.register(languageId, tokenizationSupport); +} + suite('Auto-Reindentation - TypeScript/JavaScript', () => { const languageId = LanguageId.TypeScript; @@ -59,7 +95,9 @@ suite('Auto-Reindentation - TypeScript/JavaScript', () => { disposables = new DisposableStore(); instantiationService = createModelServices(disposables); languageConfigurationService = instantiationService.get(ILanguageConfigurationService); - disposables.add(registerLanguage(languageConfigurationService, languageId)); + disposables.add(instantiationService); + disposables.add(registerLanguage(instantiationService, languageId)); + disposables.add(registerLanguageConfiguration(languageConfigurationService, languageId)); }); teardown(() => { @@ -160,7 +198,28 @@ suite('Auto-Reindentation - TypeScript/JavaScript', () => { 'const foo = `{`;', ' ', ].join('\n'); + const tokens: StandardTokenTypeData[][] = [ + [ + { startIndex: 0, standardTokenType: StandardTokenType.Other }, + { startIndex: 5, standardTokenType: StandardTokenType.Other }, + { startIndex: 6, standardTokenType: StandardTokenType.Other }, + { startIndex: 9, standardTokenType: StandardTokenType.Other }, + { startIndex: 10, standardTokenType: StandardTokenType.Other }, + { startIndex: 11, standardTokenType: StandardTokenType.Other }, + { startIndex: 12, standardTokenType: StandardTokenType.String }, + { startIndex: 13, standardTokenType: StandardTokenType.String }, + { startIndex: 14, standardTokenType: StandardTokenType.String }, + { startIndex: 15, standardTokenType: StandardTokenType.Other }, + { startIndex: 16, standardTokenType: StandardTokenType.Other } + ], + [ + { startIndex: 0, standardTokenType: StandardTokenType.Other }, + { startIndex: 4, standardTokenType: StandardTokenType.Other }] + ]; + disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); + model.tokenization.forceTokenization(1); + model.tokenization.forceTokenization(2); const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); assert.deepStrictEqual(editOperations.length, 1); const operation = editOperations[0]; @@ -258,7 +317,29 @@ suite('Auto-Reindentation - TypeScript/JavaScript', () => { 'const r = /{/;', ' ', ].join('\n'); + const tokens: StandardTokenTypeData[][] = [ + [ + { startIndex: 0, standardTokenType: StandardTokenType.Other }, + { startIndex: 5, standardTokenType: StandardTokenType.Other }, + { startIndex: 6, standardTokenType: StandardTokenType.Other }, + { startIndex: 7, standardTokenType: StandardTokenType.Other }, + { startIndex: 8, standardTokenType: StandardTokenType.Other }, + { startIndex: 9, standardTokenType: StandardTokenType.RegEx }, + { startIndex: 10, standardTokenType: StandardTokenType.RegEx }, + { startIndex: 11, standardTokenType: StandardTokenType.RegEx }, + { startIndex: 12, standardTokenType: StandardTokenType.RegEx }, + { startIndex: 13, standardTokenType: StandardTokenType.Other }, + { startIndex: 14, standardTokenType: StandardTokenType.Other } + ], + [ + { startIndex: 0, standardTokenType: StandardTokenType.Other }, + { startIndex: 4, standardTokenType: StandardTokenType.Other } + ] + ]; + disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); + model.tokenization.forceTokenization(1); + model.tokenization.forceTokenization(2); const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); assert.deepStrictEqual(editOperations.length, 1); const operation = editOperations[0]; From 0f4718cf435fea9a6ee6ebf41e338b0d0fefb1f5 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 13 May 2024 13:27:07 +0000 Subject: [PATCH 126/357] SCM - eagerly create the input box text model (#212580) * SCM - eagerly create the input box text model * Pull request feedback --- src/vs/workbench/api/browser/mainThreadSCM.ts | 43 ++++- .../workbench/api/common/extHost.protocol.ts | 2 +- .../contrib/scm/browser/scmViewPane.ts | 161 ++++++++---------- src/vs/workbench/contrib/scm/common/scm.ts | 3 +- 4 files changed, 108 insertions(+), 101 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadSCM.ts b/src/vs/workbench/api/browser/mainThreadSCM.ts index 8f2f658e810..348cd234e76 100644 --- a/src/vs/workbench/api/browser/mainThreadSCM.ts +++ b/src/vs/workbench/api/browser/mainThreadSCM.ts @@ -5,7 +5,7 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; -import { IDisposable, DisposableStore, combinedDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IDisposable, DisposableStore, combinedDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; import { ISCMService, ISCMRepository, ISCMProvider, ISCMResource, ISCMResourceGroup, ISCMResourceDecorations, IInputValidation, ISCMViewService, InputValidationType, ISCMActionButtonDescriptor } from 'vs/workbench/contrib/scm/common/scm'; import { ExtHostContext, MainThreadSCMShape, ExtHostSCMShape, SCMProviderFeatures, SCMRawResourceSplices, SCMGroupFeatures, MainContext, SCMHistoryItemGroupDto } from '../common/extHost.protocol'; import { Command } from 'vs/editor/common/languages'; @@ -20,6 +20,11 @@ import { ResourceTree } from 'vs/base/common/resourceTree'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { basename } from 'vs/base/common/resources'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { IModelService } from 'vs/editor/common/services/model'; +import { ITextModelContentProvider, ITextModelService } from 'vs/editor/common/services/resolverService'; +import { Schemas } from 'vs/base/common/network'; +import { ITextModel } from 'vs/editor/common/model'; function getIconFromIconDto(iconDto?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon): URI | { light: URI; dark: URI } | ThemeIcon | undefined { if (iconDto === undefined) { @@ -34,6 +39,25 @@ function getIconFromIconDto(iconDto?: UriComponents | { light: UriComponents; da } } +class SCMInputBoxContentProvider extends Disposable implements ITextModelContentProvider { + constructor( + textModelService: ITextModelService, + private readonly modelService: IModelService, + private readonly languageService: ILanguageService, + ) { + super(); + this._register(textModelService.registerTextModelContentProvider(Schemas.vscodeSourceControl, this)); + } + + async provideTextContent(resource: URI): Promise { + const existing = this.modelService.getModel(resource); + if (existing) { + return existing; + } + return this.modelService.createModel('', this.languageService.createById('scminput'), resource); + } +} + class MainThreadSCMResourceGroup implements ISCMResourceGroup { readonly resources: ISCMResource[] = []; @@ -197,7 +221,7 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { get handle(): number { return this._handle; } get label(): string { return this._label; } get rootUri(): URI | undefined { return this._rootUri; } - get inputBoxDocumentUri(): URI { return this._inputBoxDocumentUri; } + get inputBoxTextModel(): ITextModel { return this._inputBoxTextModel; } get contextValue(): string { return this._providerId; } get commitTemplate(): string { return this.features.commitTemplate || ''; } @@ -233,7 +257,7 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { private readonly _providerId: string, private readonly _label: string, private readonly _rootUri: URI | undefined, - private readonly _inputBoxDocumentUri: URI, + private readonly _inputBoxTextModel: ITextModel, private readonly _quickDiffService: IQuickDiffService, private readonly _uriIdentService: IUriIdentityService, private readonly _workspaceContextService: IWorkspaceContextService @@ -425,11 +449,16 @@ export class MainThreadSCM implements MainThreadSCMShape { extHostContext: IExtHostContext, @ISCMService private readonly scmService: ISCMService, @ISCMViewService private readonly scmViewService: ISCMViewService, + @ILanguageService private readonly languageService: ILanguageService, + @IModelService private readonly modelService: IModelService, + @ITextModelService private readonly textModelService: ITextModelService, @IQuickDiffService private readonly quickDiffService: IQuickDiffService, @IUriIdentityService private readonly _uriIdentService: IUriIdentityService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostSCM); + + this._disposables.add(new SCMInputBoxContentProvider(this.textModelService, this.modelService, this.languageService)); } dispose(): void { @@ -442,12 +471,16 @@ export class MainThreadSCM implements MainThreadSCMShape { this._disposables.dispose(); } - $registerSourceControl(handle: number, id: string, label: string, rootUri: UriComponents | undefined, inputBoxDocumentUri: UriComponents): void { - const provider = new MainThreadSCMProvider(this._proxy, handle, id, label, rootUri ? URI.revive(rootUri) : undefined, URI.revive(inputBoxDocumentUri), this.quickDiffService, this._uriIdentService, this.workspaceContextService); + async $registerSourceControl(handle: number, id: string, label: string, rootUri: UriComponents | undefined, inputBoxDocumentUri: UriComponents): Promise { + // Eagerly create the text model for the input box + const inputBoxTextModelRef = await this.textModelService.createModelReference(URI.revive(inputBoxDocumentUri)); + + const provider = new MainThreadSCMProvider(this._proxy, handle, id, label, rootUri ? URI.revive(rootUri) : undefined, inputBoxTextModelRef.object.textEditorModel, this.quickDiffService, this._uriIdentService, this.workspaceContextService); const repository = this.scmService.registerSCMProvider(provider); this._repositories.set(handle, repository); const disposable = combinedDisposable( + inputBoxTextModelRef, Event.filter(this.scmViewService.onDidFocusRepository, r => r === repository)(_ => this._proxy.$setSelectedSourceControl(handle)), repository.input.onDidChange(({ value }) => this._proxy.$onInputBoxValueChange(handle, value)) ); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index d7e6a87fb35..c4b0014ae38 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1517,7 +1517,7 @@ export interface SCMHistoryItemChangeDto { } export interface MainThreadSCMShape extends IDisposable { - $registerSourceControl(handle: number, id: string, label: string, rootUri: UriComponents | undefined, inputBoxDocumentUri: UriComponents): void; + $registerSourceControl(handle: number, id: string, label: string, rootUri: UriComponents | undefined, inputBoxDocumentUri: UriComponents): Promise; $updateSourceControl(handle: number, features: SCMProviderFeatures): void; $unregisterSourceControl(handle: number): void; diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 0796247d739..a9e0b07fdb2 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/scm'; import { Event, Emitter } from 'vs/base/common/event'; import { basename, dirname } from 'vs/base/common/resources'; -import { IDisposable, Disposable, DisposableStore, combinedDisposable, dispose, toDisposable, MutableDisposable, IReference, DisposableMap } from 'vs/base/common/lifecycle'; +import { IDisposable, Disposable, DisposableStore, combinedDisposable, dispose, toDisposable, MutableDisposable, DisposableMap } from 'vs/base/common/lifecycle'; import { ViewPane, IViewPaneOptions, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { append, $, Dimension, asCSSUrl, trackFocus, clearNode, prepend, isPointerEvent } from 'vs/base/browser/dom'; import { IListVirtualDelegate, IIdentityProvider } from 'vs/base/browser/ui/list/list'; @@ -43,7 +43,6 @@ import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storag import { EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; import { SIDE_BAR_BACKGROUND, PANEL_BACKGROUND } from 'vs/workbench/common/theme'; import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; -import { ITextModel } from 'vs/editor/common/model'; import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration'; import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; import { IModelService } from 'vs/editor/common/services/model'; @@ -62,7 +61,6 @@ import { LinkDetector } from 'vs/editor/contrib/links/browser/links'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; -import { ILanguageService } from 'vs/editor/common/languages/language'; import { ILabelService } from 'vs/platform/label/common/label'; import { KeyCode } from 'vs/base/common/keyCodes'; import { DEFAULT_FONT_FAMILY } from 'vs/base/browser/fonts'; @@ -86,7 +84,6 @@ import { MessageController } from 'vs/editor/contrib/message/browser/messageCont import { defaultButtonStyles, defaultCountBadgeStyles } from 'vs/platform/theme/browser/defaultStyles'; import { InlineCompletionsController } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController'; import { CodeActionController } from 'vs/editor/contrib/codeAction/browser/codeActionController'; -import { IResolvedTextEditorModel, ITextModelContentProvider, ITextModelService } from 'vs/editor/common/services/resolverService'; import { Schemas } from 'vs/base/common/network'; import { IDragAndDropData } from 'vs/base/browser/dnd'; import { fillEditorsDragData } from 'vs/workbench/browser/dnd'; @@ -111,6 +108,7 @@ import type { IUpdatableHover, IUpdatableHoverTooltipMarkdownString } from 'vs/b import { IHoverService } from 'vs/platform/hover/browser/hover'; import { OpenScmGroupAction } from 'vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver'; import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; +import { ITextModel } from 'vs/editor/common/model'; // type SCMResourceTreeNode = IResourceNode; // type SCMHistoryItemChangeResourceTreeNode = IResourceNode; @@ -390,7 +388,7 @@ class InputRenderer implements ICompressibleTreeRenderer, index: number, templateData: InputTemplate): void { const input = node.element; - templateData.inputWidget.setInput(input); + templateData.inputWidget.input = input; // Remember widget const renderedWidgets = this.inputWidgets.get(input) ?? []; @@ -2280,7 +2278,7 @@ class SCMInputWidget { private toolbar: SCMInputWidgetToolbar; private readonly disposables = new DisposableStore(); - private model: { readonly input: ISCMInput; textModelRef?: IReference } | undefined; + private model: { readonly input: ISCMInput; readonly textModel: ITextModel } | undefined; private repositoryIdContextKey: IContextKey; private readonly repositoryDisposables = new DisposableStore(); @@ -2296,11 +2294,11 @@ class SCMInputWidget { readonly onDidChangeContentHeight: Event; - private get input(): ISCMInput | undefined { + get input(): ISCMInput | undefined { return this.model?.input; } - public async setInput(input: ISCMInput | undefined) { + set input(input: ISCMInput | undefined) { if (input === this.input) { return; } @@ -2312,34 +2310,18 @@ class SCMInputWidget { this.repositoryIdContextKey.set(input?.repository.id); if (!input) { - this.model?.textModelRef?.dispose(); this.inputEditor.setModel(undefined); this.model = undefined; return; } - const uri = input.repository.provider.inputBoxDocumentUri; - if (this.configurationService.getValue('editor.wordBasedSuggestions', { resource: uri }) !== 'off') { - this.configurationService.updateValue('editor.wordBasedSuggestions', 'off', { resource: uri }, ConfigurationTarget.MEMORY); - } - - const modelValue: typeof this.model = { input, textModelRef: undefined }; - - // Save model - this.model = modelValue; - - const modelRef = await this.textModelService.createModelReference(uri); - // Model has been changed in the meantime - if (this.model !== modelValue) { - modelRef.dispose(); - return; - } - - modelValue.textModelRef = modelRef; - - const textModel = modelRef.object.textEditorModel; + const textModel = input.repository.provider.inputBoxTextModel; this.inputEditor.setModel(textModel); + if (this.configurationService.getValue('editor.wordBasedSuggestions', { resource: textModel.uri }) !== 'off') { + this.configurationService.updateValue('editor.wordBasedSuggestions', 'off', { resource: textModel.uri }, ConfigurationTarget.MEMORY); + } + // Validation const validationDelayer = new ThrottledDelayer(200); const validate = async () => { @@ -2432,6 +2414,9 @@ class SCMInputWidget { // Toolbar this.toolbar.setInput(input); + + // Save model + this.model = { input, textModel }; } get selections(): Selection[] | null { @@ -2467,7 +2452,6 @@ class SCMInputWidget { overflowWidgetsDomNode: HTMLElement, @IContextKeyService contextKeyService: IContextKeyService, @IModelService private modelService: IModelService, - @ITextModelService private textModelService: ITextModelService, @IKeybindingService private keybindingService: IKeybindingService, @IConfigurationService private configurationService: IConfigurationService, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -2738,7 +2722,7 @@ class SCMInputWidget { } dispose(): void { - this.setInput(undefined); + this.input = undefined; this.repositoryDisposables.dispose(); this.clearValidation(); this.disposables.dispose(); @@ -2880,7 +2864,6 @@ export class SCMViewPane extends ViewPane { this.storeTreeViewState(); }, this, this.disposables); - this.disposables.add(this.instantiationService.createInstance(ScmInputContentProvider)); Event.any(this.scmService.onDidAddRepository, this.scmService.onDidRemoveRepository)(() => this._onDidChangeViewWelcomeState.fire(), this, this.disposables); this.disposables.add(this.revealResourceThrottler); @@ -2929,45 +2912,49 @@ export class SCMViewPane extends ViewPane { this.onDidChangeBodyVisibility(async visible => { if (visible) { - await this.tree.setInput(this.scmViewService, viewState); + this.treeOperationSequencer.queue(async () => { + await this.tree.setInput(this.scmViewService, viewState); - Event.filter(this.configurationService.onDidChangeConfiguration, - e => - e.affectsConfiguration('scm.alwaysShowRepositories'), - this.visibilityDisposables) - (() => { - this.updateActions(); - this.updateChildren(); - }, this, this.visibilityDisposables); + Event.filter(this.configurationService.onDidChangeConfiguration, + e => + e.affectsConfiguration('scm.alwaysShowRepositories'), + this.visibilityDisposables) + (() => { + this.updateActions(); + this.updateChildren(); + }, this, this.visibilityDisposables); - Event.filter(this.configurationService.onDidChangeConfiguration, - e => - e.affectsConfiguration('scm.inputMinLineCount') || - e.affectsConfiguration('scm.inputMaxLineCount') || - e.affectsConfiguration('scm.showActionButton') || - e.affectsConfiguration('scm.showChangesSummary') || - e.affectsConfiguration('scm.showIncomingChanges') || - e.affectsConfiguration('scm.showOutgoingChanges'), - this.visibilityDisposables) - (() => this.updateChildren(), this, this.visibilityDisposables); + Event.filter(this.configurationService.onDidChangeConfiguration, + e => + e.affectsConfiguration('scm.inputMinLineCount') || + e.affectsConfiguration('scm.inputMaxLineCount') || + e.affectsConfiguration('scm.showActionButton') || + e.affectsConfiguration('scm.showChangesSummary') || + e.affectsConfiguration('scm.showIncomingChanges') || + e.affectsConfiguration('scm.showOutgoingChanges'), + this.visibilityDisposables) + (() => this.updateChildren(), this, this.visibilityDisposables); - // Add visible repositories - this.editorService.onDidActiveEditorChange(this.onDidActiveEditorChange, this, this.visibilityDisposables); - this.scmViewService.onDidChangeVisibleRepositories(this.onDidChangeVisibleRepositories, this, this.visibilityDisposables); - this.onDidChangeVisibleRepositories({ added: this.scmViewService.visibleRepositories, removed: Iterable.empty() }); + // Add visible repositories + this.editorService.onDidActiveEditorChange(this.onDidActiveEditorChange, this, this.visibilityDisposables); + this.scmViewService.onDidChangeVisibleRepositories(this.onDidChangeVisibleRepositories, this, this.visibilityDisposables); + this.onDidChangeVisibleRepositories({ added: this.scmViewService.visibleRepositories, removed: Iterable.empty() }); - // Restore scroll position - if (typeof this.treeScrollTop === 'number') { - this.tree.scrollTop = this.treeScrollTop; - this.treeScrollTop = undefined; - } + // Restore scroll position + if (typeof this.treeScrollTop === 'number') { + this.tree.scrollTop = this.treeScrollTop; + this.treeScrollTop = undefined; + } + + this.updateRepositoryCollapseAllContextKeys(); + }); } else { this.visibilityDisposables.clear(); this.onDidChangeVisibleRepositories({ added: Iterable.empty(), removed: [...this.items.keys()] }); this.treeScrollTop = this.tree.scrollTop; - } - this.updateRepositoryCollapseAllContextKeys(); + this.updateRepositoryCollapseAllContextKeys(); + } }, this, this.disposables); this.disposables.add(this.instantiationService.createInstance(RepositoryVisibilityActionController)); @@ -3492,22 +3479,28 @@ export class SCMViewPane extends ViewPane { override focus(): void { super.focus(); - if (this.isExpanded()) { - if (this.tree.getFocus().length === 0) { - for (const repository of this.scmViewService.visibleRepositories) { - const widgets = this.inputRenderer.getRenderedInputWidget(repository.input); + this.treeOperationSequencer.queue(() => { + return new Promise(resolve => { + if (this.isExpanded()) { + if (this.tree.getFocus().length === 0) { + for (const repository of this.scmViewService.visibleRepositories) { + const widgets = this.inputRenderer.getRenderedInputWidget(repository.input); - if (widgets) { - for (const widget of widgets) { - widget.focus(); + if (widgets) { + for (const widget of widgets) { + widget.focus(); + } + resolve(); + return; + } } - return; } - } - } - this.tree.domFocus(); - } + this.tree.domFocus(); + resolve(); + } + }); + }); } override dispose(): void { @@ -4002,23 +3995,3 @@ export class SCMActionButton implements IDisposable { } } } - -class ScmInputContentProvider extends Disposable implements ITextModelContentProvider { - - constructor( - @ITextModelService textModelService: ITextModelService, - @IModelService private readonly _modelService: IModelService, - @ILanguageService private readonly _languageService: ILanguageService, - ) { - super(); - this._register(textModelService.registerTextModelContentProvider(Schemas.vscodeSourceControl, this)); - } - - async provideTextContent(resource: URI): Promise { - const existing = this._modelService.getModel(resource); - if (existing) { - return existing; - } - return this._modelService.createModel('', this._languageService.createById('scminput'), resource); - } -} diff --git a/src/vs/workbench/contrib/scm/common/scm.ts b/src/vs/workbench/contrib/scm/common/scm.ts index 95bb756359d..3fe568b77d5 100644 --- a/src/vs/workbench/contrib/scm/common/scm.ts +++ b/src/vs/workbench/contrib/scm/common/scm.ts @@ -14,6 +14,7 @@ import { ThemeIcon } from 'vs/base/common/themables'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { ResourceTree } from 'vs/base/common/resourceTree'; import { ISCMHistoryProvider, ISCMHistoryProviderMenus } from 'vs/workbench/contrib/scm/common/history'; +import { ITextModel } from 'vs/editor/common/model'; export const VIEWLET_ID = 'workbench.view.scm'; export const VIEW_PANE_ID = 'workbench.scm'; @@ -70,7 +71,7 @@ export interface ISCMProvider extends IDisposable { readonly onDidChangeResources: Event; readonly rootUri?: URI; - readonly inputBoxDocumentUri: URI; + readonly inputBoxTextModel: ITextModel; readonly count?: number; readonly commitTemplate: string; readonly historyProvider?: ISCMHistoryProvider; From 6af4f79370d53df89b1b5995e40b6c169aeb1314 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 13 May 2024 16:03:44 +0200 Subject: [PATCH 127/357] aux window - detect fullscreen state early on startup (#212603) --- src/vs/platform/native/common/native.ts | 1 + .../native/electron-main/nativeHostMainService.ts | 5 +++++ src/vs/workbench/electron-sandbox/window.ts | 4 ++-- .../electron-sandbox/auxiliaryWindowService.ts | 11 ++++++++++- .../test/electron-sandbox/workbenchTestServices.ts | 1 + 5 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/vs/platform/native/common/native.ts b/src/vs/platform/native/common/native.ts index 94ae20b32f8..85e831fa04a 100644 --- a/src/vs/platform/native/common/native.ts +++ b/src/vs/platform/native/common/native.ts @@ -77,6 +77,7 @@ export interface ICommonNativeHostService { openWindow(options?: IOpenEmptyWindowOptions): Promise; openWindow(toOpen: IWindowOpenable[], options?: IOpenWindowOptions): Promise; + isFullScreen(options?: INativeHostOptions): Promise; toggleFullScreen(options?: INativeHostOptions): Promise; handleTitleDoubleClick(options?: INativeHostOptions): Promise; diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index 64737b07d00..f7235bd6834 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -212,6 +212,11 @@ export class NativeHostMainService extends Disposable implements INativeHostMain }, options); } + async isFullScreen(windowId: number | undefined, options?: INativeHostOptions): Promise { + const window = this.windowById(options?.targetWindowId, windowId); + return window?.isFullScreen ?? false; + } + async toggleFullScreen(windowId: number | undefined, options?: INativeHostOptions): Promise { const window = this.windowById(options?.targetWindowId, windowId); window?.toggleFullScreen(); diff --git a/src/vs/workbench/electron-sandbox/window.ts b/src/vs/workbench/electron-sandbox/window.ts index dfbe243f525..419b0614a76 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -253,8 +253,8 @@ export class NativeWindow extends BaseWindow { }); // Fullscreen Events - ipcRenderer.on('vscode:enterFullScreen', async () => { setFullscreen(true, mainWindow); }); - ipcRenderer.on('vscode:leaveFullScreen', async () => { setFullscreen(false, mainWindow); }); + ipcRenderer.on('vscode:enterFullScreen', () => setFullscreen(true, mainWindow)); + ipcRenderer.on('vscode:leaveFullScreen', () => setFullscreen(false, mainWindow)); // Proxy Login Dialog ipcRenderer.on('vscode:openProxyAuthenticationDialog', async (event: unknown, payload: { authInfo: AuthInfo; username?: string; password?: string; replyChannel: string }) => { diff --git a/src/vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService.ts b/src/vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService.ts index 730c5d7af6b..41d6b89e733 100644 --- a/src/vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService.ts +++ b/src/vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService.ts @@ -20,7 +20,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { Barrier } from 'vs/base/common/async'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { applyZoom } from 'vs/platform/window/electron-sandbox/window'; -import { getZoomLevel, isFullscreen } from 'vs/base/browser/browser'; +import { getZoomLevel, isFullscreen, setFullscreen } from 'vs/base/browser/browser'; import { getActiveWindow } from 'vs/base/browser/dom'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { isMacintosh } from 'vs/base/common/platform'; @@ -53,6 +53,8 @@ export class NativeAuxiliaryWindow extends AuxiliaryWindow { // transitions (Windows, Linux) via window buttons. this.handleMaximizedState(); } + + this.handleFullScreenState(); } private handleMaximizedState(): void { @@ -73,6 +75,13 @@ export class NativeAuxiliaryWindow extends AuxiliaryWindow { })); } + private async handleFullScreenState(): Promise { + const fullscreen = await this.nativeHostService.isFullScreen({ targetWindowId: this.window.vscodeWindowId }); + if (fullscreen) { + setFullscreen(true, this.window); + } + } + protected override async handleVetoBeforeClose(e: BeforeUnloadEvent, veto: string): Promise { this.preventUnload(e); diff --git a/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts b/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts index d323ca935ae..4f8d24fd9aa 100644 --- a/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts @@ -93,6 +93,7 @@ export class TestNativeHostService implements INativeHostService { async toggleFullScreen(): Promise { } async handleTitleDoubleClick(): Promise { } async isMaximized(): Promise { return true; } + async isFullScreen(): Promise { return true; } async maximizeWindow(): Promise { } async unmaximizeWindow(): Promise { } async minimizeWindow(): Promise { } From 3e769a7de739206dff7ba6ec273c5705270ceec5 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Mon, 13 May 2024 08:02:38 -0700 Subject: [PATCH 128/357] testing: polish test coverage bar (reapply) This reverts commit 4b1cc22d870a4b46f1aeec76b65e3f38a2549dca. --- src/vs/base/browser/dom.ts | 2 +- src/vs/editor/browser/editorBrowser.ts | 10 + src/vs/editor/browser/view.ts | 3 +- .../overlayWidgets/overlayWidgets.ts | 46 ++- .../widget/diffEditor/diffEditorWidget.ts | 3 +- .../editor/browser/widget/diffEditor/utils.ts | 22 +- .../stickyScroll/browser/stickyScroll.css | 1 + .../browser/stickyScrollWidget.ts | 5 +- src/vs/monaco.d.ts | 9 + .../browser/accessibilitySignalService.ts | 15 +- .../common/platformObservableUtils.ts | 30 ++ .../mergeEditor/browser/model/diffComputer.ts | 2 +- .../contrib/mergeEditor/browser/utils.ts | 13 +- .../browser/view/editors/codeEditorView.ts | 3 +- .../mergeEditor/browser/view/mergeEditor.ts | 3 +- .../mergeEditor/browser/view/viewModel.ts | 2 +- .../browser/codeCoverageDecorations.ts | 299 ++++++++++++++---- .../contrib/testing/browser/media/testing.css | 50 +-- .../testing/browser/testCoverageView.ts | 2 + .../contrib/testing/common/configuration.ts | 7 + .../contrib/testing/common/constants.ts | 1 + .../testing/common/testCoverageService.ts | 33 +- .../testing/common/testingContextKeys.ts | 1 + .../textMateWorkerTokenizerController.ts | 13 +- 24 files changed, 392 insertions(+), 183 deletions(-) create mode 100644 src/vs/platform/observable/common/platformObservableUtils.ts diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index ff113c9baa9..76abbb844ec 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -188,7 +188,7 @@ function _wrapAsStandardKeyboardEvent(handler: (e: IKeyboardEvent) => void): (e: export const addStandardDisposableListener: IAddStandardDisposableListenerSignature = function addStandardDisposableListener(node: HTMLElement, type: string, handler: (event: any) => void, useCapture?: boolean): IDisposable { let wrapHandler = handler; - if (type === 'click' || type === 'mousedown') { + if (type === 'click' || type === 'mousedown' || type === 'contextmenu') { wrapHandler = _wrapAsStandardMouseEvent(getWindow(node), handler); } else if (type === 'keydown' || type === 'keypress' || type === 'keyup') { wrapHandler = _wrapAsStandardKeyboardEvent(handler); diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index cbaa5f01d3b..2985f959335 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -250,11 +250,21 @@ export interface IOverlayWidgetPosition { * The position preference for the overlay widget. */ preference: OverlayWidgetPositionPreference | IOverlayWidgetPositionCoordinates | null; + + /** + * When set, stacks with other overlay widgets with the same preference, + * in an order determined by the ordinal value. + */ + stackOridinal?: number; } /** * An overlay widgets renders on top of the text. */ export interface IOverlayWidget { + /** + * Event fired when the widget layout changes. + */ + onDidLayout?: Event; /** * Render this overlay widget in a location where it could overflow the editor's view dom node. */ diff --git a/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts index a803234c4b4..5558cf28cd4 100644 --- a/src/vs/editor/browser/view.ts +++ b/src/vs/editor/browser/view.ts @@ -634,8 +634,7 @@ export class View extends ViewEventHandler { } public layoutOverlayWidget(widgetData: IOverlayWidgetData): void { - const newPreference = widgetData.position ? widgetData.position.preference : null; - const shouldRender = this._overlayWidgets.setWidgetPosition(widgetData.widget, newPreference); + const shouldRender = this._overlayWidgets.setWidgetPosition(widgetData.widget, widgetData.position); if (shouldRender) { this._scheduleRender(); } diff --git a/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts b/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts index 0953248e2ab..6187175e028 100644 --- a/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts +++ b/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts @@ -5,7 +5,7 @@ import 'vs/css!./overlayWidgets'; import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; -import { IOverlayWidget, IOverlayWidgetPositionCoordinates, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; +import { IOverlayWidget, IOverlayWidgetPosition, IOverlayWidgetPositionCoordinates, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; import { PartFingerprint, PartFingerprints, ViewPart } from 'vs/editor/browser/view/viewPart'; import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/browser/view/renderingContext'; import { ViewContext } from 'vs/editor/common/viewModel/viewContext'; @@ -17,6 +17,7 @@ import * as dom from 'vs/base/browser/dom'; interface IWidgetData { widget: IOverlayWidget; preference: OverlayWidgetPositionPreference | IOverlayWidgetPositionCoordinates | null; + stack?: number; domNode: FastDomNode; } @@ -109,14 +110,17 @@ export class ViewOverlayWidgets extends ViewPart { this._updateMaxMinWidth(); } - public setWidgetPosition(widget: IOverlayWidget, preference: OverlayWidgetPositionPreference | IOverlayWidgetPositionCoordinates | null): boolean { + public setWidgetPosition(widget: IOverlayWidget, position: IOverlayWidgetPosition | null): boolean { const widgetData = this._widgets[widget.getId()]; - if (widgetData.preference === preference) { + const preference = position ? position.preference : null; + const stack = position?.stackOridinal; + if (widgetData.preference === preference && widgetData.stack === stack) { this._updateMaxMinWidth(); return false; } widgetData.preference = preference; + widgetData.stack = stack; this.setShouldRender(); this._updateMaxMinWidth(); @@ -150,7 +154,7 @@ export class ViewOverlayWidgets extends ViewPart { this._context.viewLayout.setOverlayWidgetsMinWidth(maxMinWidth); } - private _renderWidget(widgetData: IWidgetData): void { + private _renderWidget(widgetData: IWidgetData, stackCoordinates: number[]): void { const domNode = widgetData.domNode; if (widgetData.preference === null) { @@ -158,16 +162,29 @@ export class ViewOverlayWidgets extends ViewPart { return; } - if (widgetData.preference === OverlayWidgetPositionPreference.TOP_RIGHT_CORNER) { - domNode.setTop(0); - domNode.setRight((2 * this._verticalScrollbarWidth) + this._minimapWidth); - } else if (widgetData.preference === OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER) { - const widgetHeight = domNode.domNode.clientHeight; - domNode.setTop((this._editorHeight - widgetHeight - 2 * this._horizontalScrollbarHeight)); - domNode.setRight((2 * this._verticalScrollbarWidth) + this._minimapWidth); + const maxRight = (2 * this._verticalScrollbarWidth) + this._minimapWidth; + if (widgetData.preference === OverlayWidgetPositionPreference.TOP_RIGHT_CORNER || widgetData.preference === OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER) { + if (widgetData.preference === OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER) { + domNode.setTop(0); + } else { + const widgetHeight = domNode.domNode.clientHeight; + domNode.setTop((this._editorHeight - widgetHeight - 2 * this._horizontalScrollbarHeight)); + } + + if (widgetData.stack !== undefined) { + domNode.setTop(stackCoordinates[widgetData.preference]); + stackCoordinates[widgetData.preference] += domNode.domNode.clientWidth; + } else { + domNode.setRight(maxRight); + } } else if (widgetData.preference === OverlayWidgetPositionPreference.TOP_CENTER) { - domNode.setTop(0); domNode.domNode.style.right = '50%'; + if (widgetData.stack !== undefined) { + domNode.setTop(stackCoordinates[OverlayWidgetPositionPreference.TOP_CENTER]); + stackCoordinates[OverlayWidgetPositionPreference.TOP_CENTER] += domNode.domNode.clientHeight; + } else { + domNode.setTop(0); + } } else { const { top, left } = widgetData.preference; const fixedOverflowWidgets = this._context.configuration.options.get(EditorOption.fixedOverflowWidgets); @@ -194,9 +211,12 @@ export class ViewOverlayWidgets extends ViewPart { this._domNode.setWidth(this._editorWidth); const keys = Object.keys(this._widgets); + const stackCoordinates = Array.from({ length: OverlayWidgetPositionPreference.TOP_CENTER + 1 }, () => 0); + keys.sort((a, b) => (this._widgets[a].stack || 0) - (this._widgets[b].stack || 0)); + for (let i = 0, len = keys.length; i < len; i++) { const widgetId = keys[i]; - this._renderWidget(this._widgets[widgetId]); + this._renderWidget(this._widgets[widgetId], stackCoordinates); } } } diff --git a/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts index fbaa5ffcabb..5fdc32e47ee 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts @@ -26,7 +26,8 @@ import { HideUnchangedRegionsFeature } from 'vs/editor/browser/widget/diffEditor import { MovedBlocksLinesFeature } from 'vs/editor/browser/widget/diffEditor/features/movedBlocksLinesFeature'; import { OverviewRulerFeature } from 'vs/editor/browser/widget/diffEditor/features/overviewRulerFeature'; import { RevertButtonsFeature } from 'vs/editor/browser/widget/diffEditor/features/revertButtonsFeature'; -import { CSSStyle, ObservableElementSizeObserver, applyStyle, applyViewZones, bindContextKey, readHotReloadableExport, translatePosition } from 'vs/editor/browser/widget/diffEditor/utils'; +import { CSSStyle, ObservableElementSizeObserver, applyStyle, applyViewZones, readHotReloadableExport, translatePosition } from 'vs/editor/browser/widget/diffEditor/utils'; +import { bindContextKey } from 'vs/platform/observable/common/platformObservableUtils'; import { IDiffEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IDimension } from 'vs/editor/common/core/dimension'; import { Position } from 'vs/editor/common/core/position'; diff --git a/src/vs/editor/browser/widget/diffEditor/utils.ts b/src/vs/editor/browser/widget/diffEditor/utils.ts index 65705cb8097..3b968353291 100644 --- a/src/vs/editor/browser/widget/diffEditor/utils.ts +++ b/src/vs/editor/browser/widget/diffEditor/utils.ts @@ -8,7 +8,7 @@ import { findLast } from 'vs/base/common/arraysFind'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { isHotReloadEnabled, registerHotReloadHandler } from 'vs/base/common/hotReload'; import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { IObservable, IReader, ISettableObservable, autorun, autorunHandleChanges, autorunOpts, autorunWithStore, observableFromEvent, observableSignalFromEvent, observableValue, transaction } from 'vs/base/common/observable'; +import { IObservable, IReader, ISettableObservable, autorun, autorunHandleChanges, autorunOpts, autorunWithStore, observableSignalFromEvent, observableValue, transaction } from 'vs/base/common/observable'; import { ElementSizeObserver } from 'vs/editor/browser/config/elementSizeObserver'; import { ICodeEditor, IOverlayWidget, IViewZone } from 'vs/editor/browser/editorBrowser'; import { Position } from 'vs/editor/common/core/position'; @@ -16,8 +16,6 @@ import { Range } from 'vs/editor/common/core/range'; import { DetailedLineRangeMapping } from 'vs/editor/common/diff/rangeMapping'; import { IModelDeltaDecoration } from 'vs/editor/common/model'; import { TextLength } from 'vs/editor/common/core/textLength'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ContextKeyValue, RawContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; export function joinCombine(arr1: readonly T[], arr2: readonly T[], keySelector: (val: T) => number, combine: (v1: T, v2: T) => T): readonly T[] { if (arr1.length === 0) { @@ -89,17 +87,6 @@ export function prependRemoveOnDispose(parent: HTMLElement, child: HTMLElement) }); } -export function observableConfigValue(key: string, defaultValue: T, configurationService: IConfigurationService): IObservable { - return observableFromEvent( - (handleChange) => configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(key)) { - handleChange(e); - } - }), - () => configurationService.getValue(key) ?? defaultValue, - ); -} - export class ObservableElementSizeObserver extends Disposable { private readonly elementSizeObserver: ElementSizeObserver; @@ -440,13 +427,6 @@ function lengthBetweenPositions(position1: Position, position2: Position): TextL } } -export function bindContextKey(key: RawContextKey, service: IContextKeyService, computeValue: (reader: IReader) => T): IDisposable { - const boundKey = key.bindTo(service); - return autorunOpts({ debugName: () => `Set Context Key "${key.key}"` }, reader => { - boundKey.set(computeValue(reader)); - }); -} - export function filterWithPrevious(arr: T[], filter: (cur: T, prev: T | undefined) => boolean): T[] { let prev: T | undefined; return arr.filter(cur => { diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css b/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css index 8afc9c241cf..3bc52c6c915 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css @@ -64,6 +64,7 @@ box-shadow: var(--vscode-editorStickyScroll-shadow) 0 3px 2px -2px; z-index: 4; background-color: var(--vscode-editorStickyScroll-background); + right: initial !important; } .monaco-editor .sticky-widget.peek { diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts index bdcaafb4891..d0e8da4b17a 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts @@ -9,7 +9,7 @@ import { equals } from 'vs/base/common/arrays'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ThemeIcon } from 'vs/base/common/themables'; import 'vs/css!./stickyScroll'; -import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; import { getColumnOfNodeOffset } from 'vs/editor/browser/viewParts/lines/viewLine'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; import { EditorLayoutInfo, EditorOption, RenderLineNumbersType } from 'vs/editor/common/config/editorOptions'; @@ -387,7 +387,8 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { getPosition(): IOverlayWidgetPosition | null { return { - preference: null + preference: OverlayWidgetPositionPreference.TOP_CENTER, + stackOridinal: 10, }; } diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index e2c5bd2ea0b..b9c7cbd73ab 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -5387,12 +5387,21 @@ declare namespace monaco.editor { * The position preference for the overlay widget. */ preference: OverlayWidgetPositionPreference | IOverlayWidgetPositionCoordinates | null; + /** + * When set, stacks with other overlay widgets with the same preference, + * in an order determined by the ordinal value. + */ + stackOridinal?: number; } /** * An overlay widgets renders on top of the text. */ export interface IOverlayWidget { + /** + * Event fired when the widget layout changes. + */ + onDidLayout?: IEvent; /** * Render this overlay widget in a location where it could overflow the editor's view dom node. */ diff --git a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts index ba277dbc24e..3ef15eb1b3a 100644 --- a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts +++ b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts @@ -8,12 +8,13 @@ import { getStructuralKey } from 'vs/base/common/equals'; import { Event, IValueWithChangeEvent } from 'vs/base/common/event'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { FileAccess } from 'vs/base/common/network'; -import { derived, IObservable, observableFromEvent } from 'vs/base/common/observable'; +import { derived, observableFromEvent } from 'vs/base/common/observable'; import { ValueWithChangeEventFromObservable } from 'vs/base/common/observableInternal/utils'; import { localize } from 'vs/nls'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; export const IAccessibilitySignalService = createDecorator('accessibilitySignalService'); @@ -201,7 +202,7 @@ export class AccessibilitySignalService extends Disposable implements IAccessibi private readonly _signalConfigValue = new CachedFunction((signal: AccessibilitySignal) => observableConfigValue<{ sound: EnabledState; announcement: EnabledState; - }>(signal.settingsKey, this.configurationService)); + }>(signal.settingsKey, { sound: 'off', announcement: 'off' }, this.configurationService)); private readonly _signalEnabledState = new CachedFunction( { getCacheKey: getStructuralKey }, @@ -589,13 +590,3 @@ export class AccessibilitySignal { }); } -export function observableConfigValue(key: string, configurationService: IConfigurationService): IObservable { - return observableFromEvent( - (handleChange) => configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(key)) { - handleChange(e); - } - }), - () => configurationService.getValue(key), - ); -} diff --git a/src/vs/platform/observable/common/platformObservableUtils.ts b/src/vs/platform/observable/common/platformObservableUtils.ts new file mode 100644 index 00000000000..096993beb80 --- /dev/null +++ b/src/vs/platform/observable/common/platformObservableUtils.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable } from 'vs/base/common/lifecycle'; +import { autorunOpts, IObservable, IReader, observableFromEvent } from 'vs/base/common/observable'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ContextKeyValue, RawContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; + +/** Creates an observable update when a configuration key updates. */ +export function observableConfigValue(key: string, defaultValue: T, configurationService: IConfigurationService): IObservable { + return observableFromEvent( + (handleChange) => configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(key)) { + handleChange(e); + } + }), + () => configurationService.getValue(key) ?? defaultValue + ); +} + +/** Update the configuration key with a value derived from observables. */ +export function bindContextKey(key: RawContextKey, service: IContextKeyService, computeValue: (reader: IReader) => T): IDisposable { + const boundKey = key.bindTo(service); + return autorunOpts({ debugName: () => `Set Context Key "${key.key}"` }, reader => { + boundKey.set(computeValue(reader)); + }); +} + diff --git a/src/vs/workbench/contrib/mergeEditor/browser/model/diffComputer.ts b/src/vs/workbench/contrib/mergeEditor/browser/model/diffComputer.ts index 278615a60b2..56760afd5eb 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/model/diffComputer.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/model/diffComputer.ts @@ -11,7 +11,7 @@ import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model/lineRange'; import { DetailedLineRangeMapping, RangeMapping } from 'vs/workbench/contrib/mergeEditor/browser/model/mapping'; -import { observableConfigValue } from 'vs/workbench/contrib/mergeEditor/browser/utils'; +import { observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; import { LineRange as DiffLineRange } from 'vs/editor/common/core/lineRange'; export interface IMergeDiffComputer { diff --git a/src/vs/workbench/contrib/mergeEditor/browser/utils.ts b/src/vs/workbench/contrib/mergeEditor/browser/utils.ts index c085272472c..5ac6522a14b 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/utils.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/utils.ts @@ -6,10 +6,9 @@ import { ArrayQueue, CompareResult } from 'vs/base/common/arrays'; import { onUnexpectedError } from 'vs/base/common/errors'; import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { IObservable, autorunOpts, observableFromEvent } from 'vs/base/common/observable'; +import { IObservable, autorunOpts } from 'vs/base/common/observable'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { IModelDeltaDecoration } from 'vs/editor/common/model'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; export function setStyle( @@ -156,13 +155,3 @@ export class PersistentStore { } } -export function observableConfigValue(key: string, defaultValue: T, configurationService: IConfigurationService): IObservable { - return observableFromEvent( - (handleChange) => configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(key)) { - handleChange(e); - } - }), - () => configurationService.getValue(key) ?? defaultValue, - ); -} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts index 8cd243e3777..29af08fbafe 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts @@ -20,7 +20,8 @@ import { MenuId } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { DEFAULT_EDITOR_MAX_DIMENSIONS, DEFAULT_EDITOR_MIN_DIMENSIONS } from 'vs/workbench/browser/parts/editor/editor'; -import { observableConfigValue, setStyle } from 'vs/workbench/contrib/mergeEditor/browser/utils'; +import { setStyle } from 'vs/workbench/contrib/mergeEditor/browser/utils'; +import { observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; import { MergeEditorViewModel } from 'vs/workbench/contrib/mergeEditor/browser/view/viewModel'; export abstract class CodeEditorView extends Disposable { diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts index 3609b0046fa..bec1dec9481 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts @@ -39,7 +39,8 @@ import { readTransientState, writeTransientState } from 'vs/workbench/contrib/co import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; import { IMergeEditorInputModel } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInputModel'; import { MergeEditorModel } from 'vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel'; -import { deepMerge, observableConfigValue, PersistentStore, thenIfNotDisposed } from 'vs/workbench/contrib/mergeEditor/browser/utils'; +import { deepMerge, PersistentStore, thenIfNotDisposed } from 'vs/workbench/contrib/mergeEditor/browser/utils'; +import { observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; import { BaseCodeEditorView } from 'vs/workbench/contrib/mergeEditor/browser/view/editors/baseCodeEditorView'; import { ScrollSynchronizer } from 'vs/workbench/contrib/mergeEditor/browser/view/scrollSynchronizer'; import { MergeEditorViewModel } from 'vs/workbench/contrib/mergeEditor/browser/view/viewModel'; diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts index ca8a00e6b56..1c0f093dc27 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts @@ -15,7 +15,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model/lineRange'; import { MergeEditorModel } from 'vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel'; import { InputNumber, ModifiedBaseRange, ModifiedBaseRangeState } from 'vs/workbench/contrib/mergeEditor/browser/model/modifiedBaseRange'; -import { observableConfigValue } from 'vs/workbench/contrib/mergeEditor/browser/utils'; +import { observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; import { BaseCodeEditorView } from 'vs/workbench/contrib/mergeEditor/browser/view/editors/baseCodeEditorView'; import { CodeEditorView } from 'vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView'; import { InputCodeEditorView } from 'vs/workbench/contrib/mergeEditor/browser/view/editors/inputCodeEditorView'; diff --git a/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts b/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts index a6ce0ce1d1c..9040875421a 100644 --- a/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts +++ b/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts @@ -4,16 +4,20 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; +import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; +import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; +import { Action } from 'vs/base/common/actions'; import { mapFindFirst } from 'vs/base/common/arraysFind'; import { assert, assertNever } from 'vs/base/common/assert'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Lazy } from 'vs/base/common/lazy'; -import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { autorun, derived, observableFromEvent, observableValue } from 'vs/base/common/observable'; import { ThemeIcon } from 'vs/base/common/themables'; -import { ICodeEditor, MouseTargetType } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, MouseTargetType, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; @@ -21,20 +25,24 @@ import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { IModelDecorationOptions, InjectedTextCursorStops, InjectedTextOptions, ITextModel } from 'vs/editor/common/model'; import { localize, localize2 } from 'vs/nls'; import { Categories } from 'vs/platform/action/common/actionCommonCategories'; -import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ILogService } from 'vs/platform/log/common/log'; +import { observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; import { IQuickInputService, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; import * as coverUtils from 'vs/workbench/contrib/testing/browser/codeCoverageDisplayUtils'; -import { testingCoverageMissingBranch } from 'vs/workbench/contrib/testing/browser/icons'; +import { testingCoverageIcon, testingCoverageMissingBranch, testingFilterIcon, testingRerunIcon } from 'vs/workbench/contrib/testing/browser/icons'; import { ManagedTestCoverageBars } from 'vs/workbench/contrib/testing/browser/testCoverageBars'; import { getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration'; +import { TestCommandId } from 'vs/workbench/contrib/testing/common/constants'; import { FileCoverage } from 'vs/workbench/contrib/testing/common/testCoverage'; import { ITestCoverageService } from 'vs/workbench/contrib/testing/common/testCoverageService'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; +import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; import { CoverageDetails, DetailType, IDeclarationCoverage, IStatementCoverage } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; @@ -52,7 +60,7 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri private loadingCancellation?: CancellationTokenSource; private readonly displayedStore = this._register(new DisposableStore()); private readonly hoveredStore = this._register(new DisposableStore()); - private readonly summaryWidget: Lazy; + private readonly summaryWidget: Lazy; private decorationIds = new Map this._register(instantiationService.createInstance(CoverageSummaryWidget, this.editor))); + this.summaryWidget = new Lazy(() => this._register(instantiationService.createInstance(CoverageToolbarWidget, this.editor))); const modelObs = observableFromEvent(editor.onDidChangeModel, () => editor.getModel()); const configObs = observableFromEvent(editor.onDidChangeConfiguration, i => i); @@ -108,6 +117,16 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri } })); + const toolbarEnabled = observableConfigValue(TestingConfigKeys.CoverageToolbarEnabled, true, configurationService); + this._register(autorun(reader => { + const c = fileCoverage.read(reader); + if (c && toolbarEnabled.read(reader)) { + this.summaryWidget.value.setCoverage(c); + } else { + this.summaryWidget.rawValue?.setCoverage(undefined); + } + })); + this._register(autorun(reader => { const c = fileCoverage.read(reader); if (c) { @@ -245,7 +264,6 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri } this.displayedStore.clear(); - this.summaryWidget.value.setCoverage(coverage); model.changeDecorations(e => { for (const detailRange of details.ranges) { @@ -309,8 +327,6 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri }); this.displayedStore.add(toDisposable(() => { - this.summaryWidget.value.setCoverage(undefined); - model.changeDecorations(e => { for (const decoration of this.decorationIds.keys()) { e.removeDecoration(decoration); @@ -513,42 +529,81 @@ function wrapName(functionNameOrCode: string) { return wrapInBackticks(functionNameOrCode); } -class CoverageSummaryWidget implements IDisposable { +class CoverageToolbarWidget extends Disposable implements IOverlayWidget { private current: FileCoverage | undefined; private registered = false; - private readonly registration = new DisposableStore(); - + private isRunning = false; + private readonly showStore = this._register(new DisposableStore()); + private readonly actionBar: ActionBar; private readonly _domNode = dom.h('div.coverage-summary-widget', [ dom.h('div', [ dom.h('span.bars@bars'), dom.h('span.stat@stat'), - dom.h('a.toggleInline@toggleInline'), - dom.h('a.perTestFilter@perTestFilter'), + dom.h('span.toolbar@toolbar'), ]), ]); private readonly bars: ManagedTestCoverageBars; - constructor( private readonly editor: ICodeEditor, @IConfigurationService private readonly configurationService: IConfigurationService, @IQuickInputService private readonly quickInputService: IQuickInputService, @ITestCoverageService private readonly testCoverageService: ITestCoverageService, - @IKeybindingService keybindingService: IKeybindingService, + @IContextMenuService private readonly contextMenuService: IContextMenuService, + @ITestService private readonly testService: ITestService, + @IKeybindingService private readonly keybindingService: IKeybindingService, @IInstantiationService instaService: IInstantiationService, ) { - this._domNode.perTestFilter.ariaLabel = this._domNode.perTestFilter.title = coverUtils.labels.clickToChangeFiltering; - this.bars = instaService.createInstance(ManagedTestCoverageBars, { + super(); + + this.bars = this._register(instaService.createInstance(ManagedTestCoverageBars, { compact: false, overall: false, container: this._domNode.bars, - }); + })); - const kb = keybindingService.lookupKeybinding(TOGGLE_INLINE_COMMAND_ID); - if (kb) { - this._domNode.toggleInline.title = `${TOGGLE_INLINE_COMMAND_TEXT} (${kb.getLabel()})`; - } + this.actionBar = this._register(instaService.createInstance(ActionBar, this._domNode.toolbar, { + orientation: ActionsOrientation.HORIZONTAL, + actionViewItemProvider: (action, options) => { + const vm = new CodiconActionViewItem(undefined, action, options); + if (action instanceof ActionWithIcon) { + vm.themeIcon = action.icon; + } + return vm; + } + })); + + + this._register(autorun(reader => { + CodeCoverageDecorations.showInline.read(reader); + this.setActions(); + })); + + this._register(dom.addStandardDisposableListener(this._domNode.root, dom.EventType.CONTEXT_MENU, e => { + this.contextMenuService.showContextMenu({ + menuId: MenuId.StickyScrollContext, + getAnchor: () => e, + }); + })); + } + + /** @inheritdoc */ + public getId(): string { + return 'coverage-summary-widget'; + } + + /** @inheritdoc */ + public getDomNode(): HTMLElement { + return this._domNode.root; + } + + /** @inheritdoc */ + public getPosition(): IOverlayWidgetPosition | null { + return { + preference: OverlayWidgetPositionPreference.TOP_CENTER, + stackOridinal: 9, + }; } public setCoverage(coverage: FileCoverage | undefined) { @@ -556,32 +611,13 @@ class CoverageSummaryWidget implements IDisposable { this.bars.setCoverageInfo(coverage); if (!coverage) { - return this.unregister(); + return this.hide(); } const displayStat = coverUtils.calculateDisplayedStat(coverage, getTestingConfiguration(this.configurationService, TestingConfigKeys.CoveragePercent)); this._domNode.stat.innerText = localize('testing.percentCoverage', '{0} Coverage', coverUtils.displayPercent(displayStat)); - - this._domNode.perTestFilter.classList.toggle('active', !!coverage.isForTest); - if (coverage.isForTest) { - const testItem = coverage.fromResult.getTestById(coverage.isForTest.id.toString()); - assert(!!testItem, 'got coverage for an unreported test'); - this._domNode.perTestFilter.style.display = 'inline'; - this._domNode.perTestFilter.innerText = coverUtils.labels.showingFilterFor(testItem.label); - } else if (coverage.perTestData?.size) { - this._domNode.perTestFilter.style.display = 'inline'; - this._domNode.perTestFilter.innerText = localize('testing.coverageForTestAvailable', "{0} test(s) in this file", coverage.perTestData.size); - } else { - this._domNode.perTestFilter.style.display = 'none'; - } - - this.register(); - } - - /** @inheritdoc */ - public dispose() { - this.unregister(); - this.bars.dispose(); + this.setActions(); + this.show(); } private filterTest() { @@ -603,65 +639,144 @@ class CoverageSummaryWidget implements IDisposable { ...tests.map(item => ({ label: coverUtils.getLabelForItem(result, item.isForTest!.id, commonPrefix), description: coverUtils.labels.percentCoverage(item.tpc), item })), ]; + // These handle the behavior that reveals the start of coverage when the + // user picks from the quickpick. Scroll position is restored if the user + // exits without picking an item, or picks "all tets". + const scrollTop = this.editor.getScrollTop(); + const revealScrollCts = new MutableDisposable(); + this.quickInputService.pick(items, { activeItem: items.find((item): item is TItem => 'item' in item && item.item === this.current), placeHolder: coverUtils.labels.pickShowCoverage, onDidFocus: (entry) => { - this.testCoverageService.filterToTest.set(entry.item?.isForTest!.id, undefined); + if (!entry.item) { + revealScrollCts.clear(); + this.editor.setScrollTop(scrollTop); + this.testCoverageService.filterToTest.set(undefined, undefined); + } else { + const cts = revealScrollCts.value = new CancellationTokenSource(); + entry.item.details(cts.token).then( + details => { + const first = details.find(d => d.type === DetailType.Statement); + if (!cts.token.isCancellationRequested && first) { + this.editor.revealLineNearTop(first.location instanceof Position ? first.location.lineNumber : first.location.startLineNumber); + } + }, + () => { /* ignored */ } + ); + this.testCoverageService.filterToTest.set(entry.item.isForTest!.id, undefined); + } }, }).then(selected => { + if (!selected) { + this.editor.setScrollTop(scrollTop); + } + + revealScrollCts.dispose(); this.testCoverageService.filterToTest.set(selected ? selected.item?.isForTest!.id : previousSelection, undefined); }); } - private register() { + private setActions() { + this.actionBar.clear(); + const coverage = this.current; + if (!coverage) { + return; + } + + const toggleAction = new ActionWithIcon( + 'toggleInline', + CodeCoverageDecorations.showInline.get() + ? localize('testing.hideInlineCoverage', 'Hide Inline Coverage') + : localize('testing.showInlineCoverage', 'Show Inline Coverage'), + testingCoverageIcon, + undefined, + () => CodeCoverageDecorations.showInline.set(!CodeCoverageDecorations.showInline.get(), undefined), + ); + + const kb = this.keybindingService.lookupKeybinding(TOGGLE_INLINE_COMMAND_ID); + if (kb) { + toggleAction.tooltip = `${TOGGLE_INLINE_COMMAND_TEXT} (${kb.getLabel()})`; + } + + this.actionBar.push(toggleAction); + + if (coverage.isForTest) { + const testItem = coverage.fromResult.getTestById(coverage.isForTest.id.toString()); + assert(!!testItem, 'got coverage for an unreported test'); + this.actionBar.push(new ActionWithIcon('perTestFilter', + coverUtils.labels.showingFilterFor(testItem.label), + testingFilterIcon, + undefined, + () => this.filterTest(), + )); + } else if (coverage.perTestData?.size) { + this.actionBar.push(new ActionWithIcon('perTestFilter', + localize('testing.coverageForTestAvailable', "{0} test(s) in this file", coverage.perTestData.size), + testingFilterIcon, + undefined, + () => this.filterTest(), + )); + } + + this.actionBar.push(new ActionWithIcon( + 'rerun', + localize('testing.rerun', 'Rerun'), + testingRerunIcon, + !this.isRunning, + () => this.rerunTest() + )); + } + + private show() { if (this.registered) { return; } this.registered = true; - let viewZoneId: string; + const ds = this.showStore; + + this.editor.addOverlayWidget(this); this.editor.changeViewZones(accessor => { - viewZoneId = accessor.addZone({ + viewZoneId = accessor.addZone({ // make space for the widget afterLineNumber: 0, afterColumn: 0, - domNode: this._domNode.root, + domNode: document.createElement('div'), heightInPx: 30, ordinal: -1, // show before code lenses }); }); - this.registration.add(toDisposable(() => { + ds.add(toDisposable(() => { + this.registered = false; + this.editor.removeOverlayWidget(this); this.editor.changeViewZones(accessor => { accessor.removeZone(viewZoneId); }); - this.registered = false; })); - this.registration.add(dom.addStandardDisposableListener(this._domNode.perTestFilter, 'click', () => { - this.filterTest(); - })); - - this.registration.add(this.configurationService.onDidChangeConfiguration(e => { + ds.add(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(TestingConfigKeys.CoverageBarThresholds) || e.affectsConfiguration(TestingConfigKeys.CoveragePercent)) { this.setCoverage(this.current); } })); - - this.registration.add(dom.addStandardDisposableListener(this._domNode.toggleInline, 'click', () => { - CodeCoverageDecorations.showInline.set(!CodeCoverageDecorations.showInline.get(), undefined); - })); - - this.registration.add(autorun(reader => { - this._domNode.toggleInline.innerText = CodeCoverageDecorations.showInline.read(reader) - ? localize('testing.hideInlineCoverage', 'Hide Inline Coverage') - : localize('testing.showInlineCoverage', 'Show Inline Coverage'); - })); } - private unregister() { - this.registration.clear(); + private rerunTest() { + const current = this.current; + if (current) { + this.isRunning = true; + this.setActions(); + this.testService.runResolvedTests(current.fromResult.request).finally(() => { + this.isRunning = false; + this.setActions(); + }); + } + } + + private hide() { + this.showStore.clear(); } } @@ -683,3 +798,47 @@ registerAction2(class ToggleInlineCoverage extends Action2 { CodeCoverageDecorations.showInline.set(!CodeCoverageDecorations.showInline.get(), undefined); } }); + +registerAction2(class ToggleCoverageToolbar extends Action2 { + constructor() { + super({ + id: TestCommandId.CoverageToggleToolbar, + title: localize2('testing.toggleToolbarTitle', "Toggle Coverage Toolbar"), + metadata: { + description: localize2('testing.toggleToolbarDesc', 'Toggle the sticky coverage bar in the editor.') + }, + category: Categories.Test, + toggled: { + condition: TestingContextKeys.coverageToolbarEnabled, + title: localize('cmd.toggle2', "Toggle Coverage Toolbar"), + }, + menu: [ + { id: MenuId.CommandPalette, when: TestingContextKeys.isTestCoverageOpen }, + { id: MenuId.StickyScrollContext, when: TestingContextKeys.isTestCoverageOpen }, + ] + }); + } + + run(accessor: ServicesAccessor): void { + const config = accessor.get(IConfigurationService); + const value = getTestingConfiguration(config, TestingConfigKeys.CoverageToolbarEnabled); + config.updateValue(TestingConfigKeys.CoverageToolbarEnabled, !value); + } +}); + +class ActionWithIcon extends Action { + constructor(id: string, title: string, public readonly icon: ThemeIcon, enabled: boolean | undefined, run: () => void) { + super(id, title, undefined, enabled, run); + } +} + +class CodiconActionViewItem extends ActionViewItem { + + public themeIcon?: ThemeIcon; + + protected override updateLabel(): void { + if (this.options.label && this.label && this.themeIcon) { + dom.reset(this.label, renderIcon(this.themeIcon), this.action.label); + } + } +} diff --git a/src/vs/workbench/contrib/testing/browser/media/testing.css b/src/vs/workbench/contrib/testing/browser/media/testing.css index 4271a033ccc..3068f3db57f 100644 --- a/src/vs/workbench/contrib/testing/browser/media/testing.css +++ b/src/vs/workbench/contrib/testing/browser/media/testing.css @@ -404,50 +404,50 @@ .coverage-summary-widget { color: var(--vscode-editor-foreground); z-index: 1; - line-height: 25px; + background: var(--vscode-editor-background); + left: 0; + width: 100%; + box-shadow: var(--vscode-editorStickyScroll-shadow) 0 3px 2px -2px; > div { display: flex; align-items: center; - border-bottom: 1px solid var(--vscode-menu-border); + padding: 0 22px; + height: 25px; } - .toggleInline, .perTestFilter { - border-left: 1px solid var(--vscode-menu-border); - padding: 0 6px; - } - - .stat, .toggleInline { - padding-right: 6px; - } - - > span, > a { - display: inline; + .btn { position: relative; - padding: 0 6px; + margin: 0 4px; + padding: 0 4px; &:first-child { - padding-left: 0; + margin-left: 0; } &:last-child { - padding-right: 0; + margin-right: 0; } } - - a { - color: var(--vscode-textLink-foreground); - cursor: pointer; + .stat, .action-label { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + margin: 0 3px; } - a:hover { - color: var(--vscode-textLink-activeForeground); + .action-label { + display: flex; + align-items: center; + font-size: 13px; + padding: 0 4px; + + .codicon { + margin-right: 4px; + } } - .toggleInline, .perTestFilter { - border-left: 1px solid var(--vscode-menu-border); - } } .test-coverage-tree-per-test-switcher { diff --git a/src/vs/workbench/contrib/testing/browser/testCoverageView.ts b/src/vs/workbench/contrib/testing/browser/testCoverageView.ts index ff7bda28075..841903f8096 100644 --- a/src/vs/workbench/contrib/testing/browser/testCoverageView.ts +++ b/src/vs/workbench/contrib/testing/browser/testCoverageView.ts @@ -23,6 +23,7 @@ import { URI } from 'vs/base/common/uri'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { localize, localize2 } from 'vs/nls'; +import { Categories } from 'vs/platform/action/common/actionCommonCategories'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -685,6 +686,7 @@ registerAction2(class TestCoverageChangePerTestFilterAction extends Action2 { constructor() { super({ id: TestCommandId.CoverageFilterToTest, + category: Categories.Test, title: localize2('testing.changeCoverageFilter', 'Filter Coverage by Test...'), precondition: TestingContextKeys.hasPerTestCoverage, f1: true, diff --git a/src/vs/workbench/contrib/testing/common/configuration.ts b/src/vs/workbench/contrib/testing/common/configuration.ts index 1bbc290e9cd..ec9e50d67f0 100644 --- a/src/vs/workbench/contrib/testing/common/configuration.ts +++ b/src/vs/workbench/contrib/testing/common/configuration.ts @@ -23,6 +23,7 @@ export const enum TestingConfigKeys { CoveragePercent = 'testing.displayedCoveragePercent', ShowCoverageInExplorer = 'testing.showCoverageInExplorer', CoverageBarThresholds = 'testing.coverageBarThresholds', + CoverageToolbarEnabled = 'testing.coverageToolbarEnabled', } export const enum AutoOpenTesting { @@ -190,6 +191,11 @@ export const testingConfiguration: IConfigurationNode = { green: { type: 'number', minimum: 0, maximum: 100, default: 90 }, }, }, + [TestingConfigKeys.CoverageToolbarEnabled]: { + description: localize('testing.coverageToolbarEnabled', 'Controls whether the coverage toolbar is shown in the editor.'), + type: 'boolean', + default: false, // todo@connor4312: disabled by default until UI sync + }, } }; @@ -214,6 +220,7 @@ export interface ITestingConfiguration { [TestingConfigKeys.CoveragePercent]: TestingDisplayedCoveragePercent; [TestingConfigKeys.ShowCoverageInExplorer]: boolean; [TestingConfigKeys.CoverageBarThresholds]: ITestingCoverageBarThresholds; + [TestingConfigKeys.CoverageToolbarEnabled]: boolean; } export const getTestingConfiguration = (config: IConfigurationService, key: K) => config.getValue(key); diff --git a/src/vs/workbench/contrib/testing/common/constants.ts b/src/vs/workbench/contrib/testing/common/constants.ts index 2dfd8cf55c2..e879003b6a1 100644 --- a/src/vs/workbench/contrib/testing/common/constants.ts +++ b/src/vs/workbench/contrib/testing/common/constants.ts @@ -68,6 +68,7 @@ export const enum TestCommandId { CoverageFilterToTest = 'testing.coverageFilterToTest', CoverageLastRun = 'testing.coverageLastRun', CoverageSelectedAction = 'testing.coverageSelected', + CoverageToggleToolbar = 'testing.coverageToggleToolbar', CoverageViewChangeSorting = 'testing.coverageViewChangeSorting', DebugAction = 'testing.debug', DebugAllAction = 'testing.debugAll', diff --git a/src/vs/workbench/contrib/testing/common/testCoverageService.ts b/src/vs/workbench/contrib/testing/common/testCoverageService.ts index 1336f748f86..99e86e86a95 100644 --- a/src/vs/workbench/contrib/testing/common/testCoverageService.ts +++ b/src/vs/workbench/contrib/testing/common/testCoverageService.ts @@ -6,8 +6,11 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { IObservable, ISettableObservable, observableValue, transaction } from 'vs/base/common/observable'; -import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { bindContextKey, observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; +import { TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration'; import { Testing } from 'vs/workbench/contrib/testing/common/constants'; import { TestCoverage } from 'vs/workbench/contrib/testing/common/testCoverage'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; @@ -45,8 +48,6 @@ export interface ITestCoverageService { export class TestCoverageService extends Disposable implements ITestCoverageService { declare readonly _serviceBrand: undefined; - private readonly _isOpenKey: IContextKey; - private readonly _hasPerTestCoverage: IContextKey; private readonly lastOpenCts = this._register(new MutableDisposable()); public readonly selected = observableValue('testCoverage', undefined); @@ -55,11 +56,29 @@ export class TestCoverageService extends Disposable implements ITestCoverageServ constructor( @IContextKeyService contextKeyService: IContextKeyService, @ITestResultService resultService: ITestResultService, + @IConfigurationService configService: IConfigurationService, @IViewsService private readonly viewsService: IViewsService, ) { super(); - this._isOpenKey = TestingContextKeys.isTestCoverageOpen.bindTo(contextKeyService); - this._hasPerTestCoverage = TestingContextKeys.hasPerTestCoverage.bindTo(contextKeyService); + + const toolbarConfig = observableConfigValue(TestingConfigKeys.CoverageToolbarEnabled, true, configService); + this._register(bindContextKey( + TestingContextKeys.coverageToolbarEnabled, + contextKeyService, + reader => toolbarConfig.read(reader), + )); + + this._register(bindContextKey( + TestingContextKeys.isTestCoverageOpen, + contextKeyService, + reader => !!this.selected.read(reader), + )); + + this._register(bindContextKey( + TestingContextKeys.hasPerTestCoverage, + contextKeyService, + reader => !!this.selected.read(reader)?.perTestCoverageIDs.size, + )); this._register(resultService.onResultsChanged(evt => { if ('completed' in evt) { @@ -92,8 +111,6 @@ export class TestCoverageService extends Disposable implements ITestCoverageServ this.filterToTest.set(undefined, tx); this.selected.set(coverage, tx); }); - this._isOpenKey.set(true); - this._hasPerTestCoverage.set(coverage.perTestCoverageIDs.size > 0); if (focus && !cts.token.isCancellationRequested) { this.viewsService.openView(Testing.CoverageViewId, true); @@ -102,8 +119,6 @@ export class TestCoverageService extends Disposable implements ITestCoverageServ /** @inheritdoc */ public closeCoverage() { - this._isOpenKey.set(false); - this._hasPerTestCoverage.set(false); this.selected.set(undefined, undefined); } } diff --git a/src/vs/workbench/contrib/testing/common/testingContextKeys.ts b/src/vs/workbench/contrib/testing/common/testingContextKeys.ts index 7878be0ec9e..2c3d0b8c79f 100644 --- a/src/vs/workbench/contrib/testing/common/testingContextKeys.ts +++ b/src/vs/workbench/contrib/testing/common/testingContextKeys.ts @@ -23,6 +23,7 @@ export namespace TestingContextKeys { export const activeEditorHasTests = new RawContextKey('testing.activeEditorHasTests', false, { type: 'boolean', description: localize('testing.activeEditorHasTests', 'Indicates whether any tests are present in the current editor') }); export const isTestCoverageOpen = new RawContextKey('testing.isTestCoverageOpen', false, { type: 'boolean', description: localize('testing.isTestCoverageOpen', 'Indicates whether a test coverage report is open') }); export const hasPerTestCoverage = new RawContextKey('testing.hasPerTestCoverage', false, { type: 'boolean', description: localize('testing.hasPerTestCoverage', 'Indicates whether per-test coverage is available') }); + export const coverageToolbarEnabled = new RawContextKey('testing.coverageToolbarEnabled', true, { type: 'boolean', description: localize('testing.coverageToolbarEnabled', 'Indicates whether the coverage toolbar is enabled') }); export const capabilityToContextKey: { [K in TestRunProfileBitset]: RawContextKey } = { [TestRunProfileBitset.Run]: hasRunnableTests, diff --git a/src/vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController.ts b/src/vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController.ts index 850b58e1e6c..3695379f0e9 100644 --- a/src/vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController.ts +++ b/src/vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController.ts @@ -5,7 +5,7 @@ import { importAMDNodeModule } from 'vs/amdX'; import { Disposable } from 'vs/base/common/lifecycle'; -import { IObservable, autorun, keepObserved, observableFromEvent } from 'vs/base/common/observable'; +import { IObservable, autorun, keepObserved } from 'vs/base/common/observable'; import { countEOL } from 'vs/editor/common/core/eolCounter'; import { LineRange } from 'vs/editor/common/core/lineRange'; import { Range } from 'vs/editor/common/core/range'; @@ -15,6 +15,7 @@ import { TokenizationStateStore } from 'vs/editor/common/model/textModelTokens'; import { IModelContentChange, IModelContentChangedEvent } from 'vs/editor/common/textModelEvents'; import { ContiguousMultilineTokensBuilder } from 'vs/editor/common/tokens/contiguousMultilineTokensBuilder'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; import { ArrayEdit, MonotonousIndexTransformer, SingleArrayEdit } from 'vs/workbench/services/textMate/browser/arrayOperation'; import type { StateDeltas, TextMateTokenizationWorker } from 'vs/workbench/services/textMate/browser/backgroundTokenization/worker/textMateTokenizationWorker.worker'; import type { applyStateStackDiff, StateStack } from 'vscode-textmate'; @@ -237,13 +238,3 @@ function changesToString(changes: IModelContentChange[]): string { return changes.map(c => Range.lift(c.range).toString() + ' => ' + c.text).join(' & '); } -function observableConfigValue(key: string, defaultValue: T, configurationService: IConfigurationService): IObservable { - return observableFromEvent( - (handleChange) => configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(key)) { - handleChange(e); - } - }), - () => configurationService.getValue(key) ?? defaultValue, - ); -} From 89d8722bddbbf8d80630600c380307691a66e70e Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Mon, 13 May 2024 08:14:47 -0700 Subject: [PATCH 129/357] editor: fix swapped condition in overlay widget --- .../editor/browser/viewParts/overlayWidgets/overlayWidgets.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts b/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts index 6187175e028..5b3e86a042d 100644 --- a/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts +++ b/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts @@ -165,10 +165,10 @@ export class ViewOverlayWidgets extends ViewPart { const maxRight = (2 * this._verticalScrollbarWidth) + this._minimapWidth; if (widgetData.preference === OverlayWidgetPositionPreference.TOP_RIGHT_CORNER || widgetData.preference === OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER) { if (widgetData.preference === OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER) { - domNode.setTop(0); - } else { const widgetHeight = domNode.domNode.clientHeight; domNode.setTop((this._editorHeight - widgetHeight - 2 * this._horizontalScrollbarHeight)); + } else { + domNode.setTop(0); } if (widgetData.stack !== undefined) { From e13f3f5b24e1491da849e130c0c6998594b12009 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 13 May 2024 15:24:19 +0000 Subject: [PATCH 130/357] SCM - revert sticky scroll changes for the input/action button nodes (#212607) * Revert "SCM Action Button" This reverts commit 630a9f8c5d1a57eefe649ed0d1cb6f8d68685fe3. * Revert "SCM InputRenderer" This reverts commit 4c24d98656e3e702bef9f153c403ad0a3a7f30ae. * Manually fix merge conflict --- .../contrib/scm/browser/scmViewPane.ts | 78 +++++-------------- 1 file changed, 20 insertions(+), 58 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index a9e0b07fdb2..b7df9b4c0e7 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -223,7 +223,7 @@ export class ActionButtonRenderer implements ICompressibleTreeRenderer(); + private actionButtons = new Map(); constructor( @ICommandService private commandService: ICommandService, @@ -252,24 +252,8 @@ export class ActionButtonRenderer implements ICompressibleTreeRenderer { - const renderedActionButtons = this.actionButtons.get(actionButton) ?? []; - const renderedWidgetIndex = renderedActionButtons.findIndex(renderedActionButton => renderedActionButton === templateData.actionButton); - - if (renderedWidgetIndex < 0) { - throw new Error('Disposing unknown action button'); - } - - if (renderedActionButtons.length === 1) { - this.actionButtons.delete(actionButton); - } else { - renderedActionButtons.splice(renderedWidgetIndex, 1); - } - } - }); + this.actionButtons.set(actionButton, templateData.actionButton); + disposables.add({ dispose: () => this.actionButtons.delete(actionButton) }); templateData.disposable = disposables; } @@ -279,7 +263,7 @@ export class ActionButtonRenderer implements ICompressibleTreeRenderer renderedActionButton.focus()); + this.actionButtons.get(actionButton)?.focus(); } disposeElement(node: ITreeNode, index: number, template: ActionButtonTemplate): void { @@ -360,7 +344,7 @@ class InputRenderer implements ICompressibleTreeRenderer(); + private inputWidgets = new Map(); private contentHeights = new WeakMap(); private editorSelections = new WeakMap(); @@ -391,23 +375,9 @@ class InputRenderer implements ICompressibleTreeRenderer { - const renderedWidgets = this.inputWidgets.get(input) ?? []; - const renderedWidgetIndex = renderedWidgets.findIndex(renderedWidget => renderedWidget === templateData.inputWidget); - - if (renderedWidgetIndex < 0) { - throw new Error('Disposing unknown input widget'); - } - - if (renderedWidgets.length === 1) { - this.inputWidgets.delete(input); - } else { - renderedWidgets.splice(renderedWidgetIndex, 1); - } - } + dispose: () => this.inputWidgets.delete(input) }); // Widget cursor selections @@ -470,16 +440,14 @@ class InputRenderer implements ICompressibleTreeRenderer widget.focus()); + this.inputRenderer.getRenderedInputWidget(focusedInput)?.focus(); } this.updateScmProviderContextKeys(); @@ -3484,12 +3448,10 @@ export class SCMViewPane extends ViewPane { if (this.isExpanded()) { if (this.tree.getFocus().length === 0) { for (const repository of this.scmViewService.visibleRepositories) { - const widgets = this.inputRenderer.getRenderedInputWidget(repository.input); + const widget = this.inputRenderer.getRenderedInputWidget(repository.input); - if (widgets) { - for (const widget of widgets) { - widget.focus(); - } + if (widget) { + widget.focus(); resolve(); return; } From 3fdda617d3ae6ce9467deeff4e6af978527ff068 Mon Sep 17 00:00:00 2001 From: BrunoSoaresEngineering <122215977+BrunoSoaresEngineering@users.noreply.github.com> Date: Mon, 13 May 2024 16:29:10 +0100 Subject: [PATCH 131/357] feat(markdown-language-features): #208398 add avif as image extension (#212547) --- .../src/languageFeatures/copyFiles/shared.ts | 1 + extensions/markdown-language-features/src/util/mimes.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts b/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts index fc232bfeecb..4ab245c192b 100644 --- a/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts +++ b/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts @@ -20,6 +20,7 @@ enum MediaKind { export const mediaFileExtensions = new Map([ // Images + ['avif', MediaKind.Image], ['bmp', MediaKind.Image], ['gif', MediaKind.Image], ['ico', MediaKind.Image], diff --git a/extensions/markdown-language-features/src/util/mimes.ts b/extensions/markdown-language-features/src/util/mimes.ts index 8028294b3f4..f33b807b83e 100644 --- a/extensions/markdown-language-features/src/util/mimes.ts +++ b/extensions/markdown-language-features/src/util/mimes.ts @@ -9,6 +9,7 @@ export const Mime = { } as const; export const mediaMimes = new Set([ + 'image/avif', 'image/bmp', 'image/gif', 'image/jpeg', From ce786ed366c18cd6ac54e061864457fa7e4e3bae Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 13 May 2024 17:44:39 +0200 Subject: [PATCH 132/357] fix #212420 (#212611) --- .../contrib/extensions/browser/media/extensionActions.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionActions.css b/src/vs/workbench/contrib/extensions/browser/media/extensionActions.css index b2eaa27034a..7bca4243703 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionActions.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionActions.css @@ -44,7 +44,7 @@ color: var(--vscode-extensionButton-foreground) !important; } -.monaco-action-bar .action-item .action-label.extension-action.label:hover { +.monaco-action-bar .action-item:not(.disabled) .action-label.extension-action.label:hover { background-color: var(--vscode-extensionButton-hoverBackground) !important; } @@ -61,7 +61,7 @@ color: var(--vscode-extensionButton-prominentForeground) !important; } -.monaco-action-bar .action-item .action-label.extension-action.label.prominent:hover { +.monaco-action-bar .action-item.action-item:not(.disabled) .action-label.extension-action.label.prominent:hover { background-color: var(--vscode-extensionButton-prominentHoverBackground); } From fb618e4e606c68f97e21027ba4f6ccf227d1b10d Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 13 May 2024 18:22:54 +0200 Subject: [PATCH 133/357] add source context to extension installation (#212615) --- .../abstractExtensionManagementService.ts | 34 ++++++++++++++++--- .../common/extensionManagement.ts | 1 + .../browser/extensions.contribution.ts | 6 ++-- 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts index 95438e4a764..f797cd72e80 100644 --- a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts +++ b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts @@ -17,7 +17,8 @@ import { ExtensionManagementError, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementParticipant, IGalleryExtension, ILocalExtension, InstallOperation, IExtensionsControlManifest, StatisticType, isTargetPlatformCompatible, TargetPlatformToString, ExtensionManagementErrorCode, InstallOptions, UninstallOptions, Metadata, InstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, IExtensionManagementService, InstallExtensionInfo, EXTENSION_INSTALL_DEP_PACK_CONTEXT, ExtensionGalleryError, - IProductVersion, ExtensionGalleryErrorCode + IProductVersion, ExtensionGalleryErrorCode, + EXTENSION_INSTALL_SOURCE_CONTEXT } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions, ExtensionKey, getGalleryExtensionId, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionType, IExtensionManifest, isApplicationScopedExtension, TargetPlatform } from 'vs/platform/extensions/common/extensions'; @@ -297,7 +298,11 @@ export abstract class AbstractExtensionManagementService extends Disposable impl } catch (e) { const error = toExtensionManagementError(e); if (!URI.isUri(task.source)) { - reportTelemetry(this.telemetryService, task.operation === InstallOperation.Update ? 'extensionGallery:update' : 'extensionGallery:install', { extensionData: getGalleryExtensionTelemetryData(task.source), error }); + reportTelemetry(this.telemetryService, task.operation === InstallOperation.Update ? 'extensionGallery:update' : 'extensionGallery:install', { + extensionData: getGalleryExtensionTelemetryData(task.source), + error, + source: task.options.context?.[EXTENSION_INSTALL_SOURCE_CONTEXT] + }); } installExtensionResultsMap.set(key, { error, identifier: task.identifier, operation: task.operation, source: task.source, context: task.options.context, profileLocation: task.profileLocation, applicationScoped: task.options.isApplicationScoped }); this.logService.error('Error while installing the extension', task.identifier.id, getErrorMessage(error)); @@ -310,7 +315,8 @@ export abstract class AbstractExtensionManagementService extends Disposable impl extensionData: getGalleryExtensionTelemetryData(task.source), verificationStatus: task.verificationStatus, duration: new Date().getTime() - startTime, - durationSinceUpdate + durationSinceUpdate, + source: task.options.context?.[EXTENSION_INSTALL_SOURCE_CONTEXT] }); // In web, report extension install statistics explicitly. In Desktop, statistics are automatically updated while downloading the VSIX. if (isWeb && task.operation !== InstallOperation.Update) { @@ -801,7 +807,23 @@ export function toExtensionManagementError(error: Error, code?: ExtensionManagem return extensionManagementError; } -function reportTelemetry(telemetryService: ITelemetryService, eventName: string, { extensionData, verificationStatus, duration, error, durationSinceUpdate }: { extensionData: any; verificationStatus?: ExtensionVerificationStatus; duration?: number; durationSinceUpdate?: number; error?: ExtensionManagementError | ExtensionGalleryError }): void { +function reportTelemetry(telemetryService: ITelemetryService, eventName: string, + { + extensionData, + verificationStatus, + duration, + error, + source, + durationSinceUpdate + }: { + extensionData: any; + verificationStatus?: + ExtensionVerificationStatus; + duration?: number; + durationSinceUpdate?: number; + source?: string; + error?: ExtensionManagementError | ExtensionGalleryError; + }): void { let errorcode: string | undefined; let errorcodeDetail: string | undefined; @@ -834,6 +856,7 @@ function reportTelemetry(telemetryService: ITelemetryService, eventName: string, "errorcodeDetail": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, "recommendationReason": { "retiredFromVersion": "1.23.0", "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "verificationStatus" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "source": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "${include}": [ "${GalleryExtensionTelemetryData}" ] @@ -858,12 +881,13 @@ function reportTelemetry(telemetryService: ITelemetryService, eventName: string, "errorcode": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, "errorcodeDetail": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, "verificationStatus" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "source": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "${include}": [ "${GalleryExtensionTelemetryData}" ] } */ - telemetryService.publicLog(eventName, { ...extensionData, verificationStatus, success: !error, duration, errorcode, errorcodeDetail, durationSinceUpdate }); + telemetryService.publicLog(eventName, { ...extensionData, verificationStatus, success: !error, duration, errorcode, errorcodeDetail, durationSinceUpdate, source }); } export abstract class AbstractExtensionTask { diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 4249e27a561..9ae308b2c9e 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -18,6 +18,7 @@ export const EXTENSION_IDENTIFIER_REGEX = new RegExp(EXTENSION_IDENTIFIER_PATTER export const WEB_EXTENSION_TAG = '__web_extension'; export const EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT = 'skipWalkthrough'; export const EXTENSION_INSTALL_SYNC_CONTEXT = 'extensionsSync'; +export const EXTENSION_INSTALL_SOURCE_CONTEXT = 'extensionInstallSource'; export const EXTENSION_INSTALL_DEP_PACK_CONTEXT = 'dependecyOrPackExtensionInstall'; export const EXTENSION_INSTALL_CLIENT_TARGET_PLATFORM_CONTEXT = 'clientTargetPlatform'; diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index b38717d2a21..c9c19453de6 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -8,7 +8,7 @@ import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { Registry } from 'vs/platform/registry/common/platform'; import { MenuRegistry, MenuId, registerAction2, Action2, ISubmenuItem, IMenuItem, IAction2Options } from 'vs/platform/actions/common/actions'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { ExtensionsLocalizedLabel, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionsLocalizedLabel, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel, InstallOperation, EXTENSION_INSTALL_SOURCE_CONTEXT } from 'vs/platform/extensionManagement/common/extensionManagement'; import { EnablementState, IExtensionManagementServerService, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService, extensionsConfigurationNodeBase } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; @@ -365,13 +365,13 @@ CommandsRegistry.registerCommand({ isMachineScoped: options?.donotSync ? true : undefined, /* do not allow syncing extensions automatically while installing through the command */ installPreReleaseVersion: options?.installPreReleaseVersion, installGivenVersion: !!version, - context: options?.context + context: { ...options?.context, [EXTENSION_INSTALL_SOURCE_CONTEXT]: 'command' }, }); } else { await extensionsWorkbenchService.install(arg, { version, installPreReleaseVersion: options?.installPreReleaseVersion, - context: options?.context, + context: { ...options?.context, [EXTENSION_INSTALL_SOURCE_CONTEXT]: 'command' }, justification: options?.justification, enable: options?.enable, isMachineScoped: options?.donotSync ? true : undefined, /* do not allow syncing extensions automatically while installing through the command */ From 3b72461942f9bf6036fea69da81f1783a45ed832 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Mon, 13 May 2024 09:47:59 -0700 Subject: [PATCH 134/357] fix: don't show spinner when doing basic render of chat progress task --- src/vs/workbench/contrib/chat/browser/chatListRenderer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index c1a02143150..687d7361c98 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -456,7 +456,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer Date: Mon, 13 May 2024 18:57:43 +0200 Subject: [PATCH 135/357] more API todos (#212617) --- src/vscode-dts/vscode.proposed.languageModels.d.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/vscode-dts/vscode.proposed.languageModels.d.ts b/src/vscode-dts/vscode.proposed.languageModels.d.ts index 11c0e2ade17..cae4cb4f135 100644 --- a/src/vscode-dts/vscode.proposed.languageModels.d.ts +++ b/src/vscode-dts/vscode.proposed.languageModels.d.ts @@ -243,7 +243,7 @@ declare module 'vscode' { /** * Options for making a chat request using a language model. * - * @see {@link lm.chatRequest} + * @see {@link LanguageModelChat.sendRequest} */ export interface LanguageModelChatRequestOptions { @@ -279,6 +279,7 @@ declare module 'vscode' { * @param selector A chat model selector. When omitted all chat models are returned. * @returns An array of chat models or `undefined` when no chat model was selected. */ + // TODO@API no undefined but empty array export function selectChatModels(selector?: LanguageModelChatSelector): Thenable; } @@ -302,6 +303,8 @@ declare module 'vscode' { * model does not exist or consent hasn't been asked for. */ // TODO@API applies to chat and embeddings models + // TODO@API use LanguageModelChat + // TODO@API name: canUse, hasAccess? canSendRequest(languageModelId: string): boolean | undefined; } From 95e2edd48f91bcd2429081f1f4cec9e2eb265a7c Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 13 May 2024 19:02:13 +0200 Subject: [PATCH 136/357] `ChatProviderInvokedEvent` event should include the location at which chat was created/started (#212618) https://github.com/microsoft/vscode-copilot/issues/5604 --- src/vs/workbench/contrib/chat/common/chatServiceImpl.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index 1efa8f9e2e3..c7ab1fdb48c 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -51,6 +51,7 @@ type ChatProviderInvokedEvent = { chatSessionId: string; agent: string; slashCommand: string | undefined; + location: ChatAgentLocation; }; type ChatProviderInvokedClassification = { @@ -61,6 +62,7 @@ type ChatProviderInvokedClassification = { chatSessionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'A random ID for the session.' }; agent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of agent used.' }; slashCommand?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of slashCommand used.' }; + location?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The location at which chat request was made.' }; owner: 'roblourens'; comment: 'Provides insight into the performance of Chat agents.'; }; @@ -548,7 +550,8 @@ export class ChatService extends Disposable implements IChatService { requestType, agent: agentPart?.agent.id ?? '', slashCommand: agentSlashCommandPart ? agentSlashCommandPart.command.name : commandPart?.slashCommand.command, - chatSessionId: model.sessionId + chatSessionId: model.sessionId, + location, }); model.cancelRequest(request); @@ -642,7 +645,8 @@ export class ChatService extends Disposable implements IChatService { requestType, agent: agentPart?.agent.id ?? '', slashCommand: agentSlashCommandPart ? agentSlashCommandPart.command.name : commandPart?.slashCommand.command, - chatSessionId: model.sessionId + chatSessionId: model.sessionId, + location }); model.setResponse(request, rawResult); completeResponseCreated(); From 59018d4606c2b031616303efa9d81c5b5420b4c4 Mon Sep 17 00:00:00 2001 From: Aaron Munger Date: Mon, 13 May 2024 10:17:33 -0700 Subject: [PATCH 137/357] always resume the pausable emitter (#212621) * keep the resume call for corresponding pause * always resume the pausable emitter --- .../common/model/notebookTextModel.ts | 48 +++++++++++-------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index 9ee44dcf0c7..b3b6ff6e2f9 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -77,32 +77,38 @@ class StackOperation implements IWorkspaceUndoRedoElement { async undo(): Promise { this._pauseableEmitter.pause(); - for (let i = this._operations.length - 1; i >= 0; i--) { - await this._operations[i].undo(); + try { + for (let i = this._operations.length - 1; i >= 0; i--) { + await this._operations[i].undo(); + } + this._postUndoRedo(this._beginAlternativeVersionId); + this._pauseableEmitter.fire({ + rawEvents: [], + synchronous: undefined, + versionId: this.textModel.versionId, + endSelectionState: this._beginSelectionState + }); + } finally { + this._pauseableEmitter.resume(); } - this._postUndoRedo(this._beginAlternativeVersionId); - this._pauseableEmitter.fire({ - rawEvents: [], - synchronous: undefined, - versionId: this.textModel.versionId, - endSelectionState: this._beginSelectionState - }); - this._pauseableEmitter.resume(); } async redo(): Promise { this._pauseableEmitter.pause(); - for (let i = 0; i < this._operations.length; i++) { - await this._operations[i].redo(); + try { + for (let i = 0; i < this._operations.length; i++) { + await this._operations[i].redo(); + } + this._postUndoRedo(this._resultAlternativeVersionId); + this._pauseableEmitter.fire({ + rawEvents: [], + synchronous: undefined, + versionId: this.textModel.versionId, + endSelectionState: this._resultSelectionState + }); + } finally { + this._pauseableEmitter.resume(); } - this._postUndoRedo(this._resultAlternativeVersionId); - this._pauseableEmitter.fire({ - rawEvents: [], - synchronous: undefined, - versionId: this.textModel.versionId, - endSelectionState: this._resultSelectionState - }); - this._pauseableEmitter.resume(); } } @@ -527,8 +533,8 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel // Broadcast changes this._pauseableEmitter.fire({ rawEvents: [], versionId: this.versionId, synchronous: synchronous, endSelectionState: endSelections }); - this._pauseableEmitter.resume(); } + this._pauseableEmitter.resume(); } } From 3497513f6bf0f0753272ae02fcbbee0c719c810b Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Mon, 13 May 2024 10:34:08 -0700 Subject: [PATCH 138/357] set terminal hint font, font size to that of terminal, fix bug (#212623) --- .../browser/terminal.initialHint.contribution.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts index 41f58f9179d..8fc93f1577c 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Disposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; -import { IDetachedTerminalInstance, ITerminalContribution, ITerminalInstance, ITerminalService, IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { IDetachedTerminalInstance, ITerminalContribution, ITerminalEditorService, ITerminalGroupService, ITerminalInstance, ITerminalService, IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal'; import { registerTerminalContribution } from 'vs/workbench/contrib/terminal/browser/terminalExtensions'; import type { Terminal as RawXtermTerminal, IDecoration, ITerminalAddon } from '@xterm/xterm'; import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager'; @@ -83,13 +83,14 @@ export class TerminalInitialHintContribution extends Disposable implements ITerm @IInlineChatService private readonly _inlineChatService: IInlineChatService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IConfigurationService private readonly _configurationService: IConfigurationService, - @ITerminalService private readonly _terminalService: ITerminalService + @ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService, + @ITerminalEditorService private readonly _terminalEditorService: ITerminalEditorService, ) { super(); } xtermOpen(xterm: IXtermTerminal & { raw: RawXtermTerminal }): void { - if (this._terminalService.instances.length !== 1) { + if (this._terminalGroupService.instances.length + this._terminalEditorService.instances.length !== 1) { // only show for the first terminal return; } @@ -135,7 +136,7 @@ export class TerminalInitialHintContribution extends Disposable implements ITerm } this._register(this._decoration); this._register(this._decoration.onRender((e) => { - if (!this._hintWidget && this._xterm?.isFocused && this._terminalService.instances.length === 1) { + if (!this._hintWidget && this._xterm?.isFocused && this._terminalGroupService.instances.length + this._terminalEditorService.instances.length === 1) { const chatProviders = [...this._inlineChatService.getAllProvider()]; if (chatProviders?.length) { const widget = this._register(this._instantiationService.createInstance(TerminalInitialHintWidget, instance)); @@ -146,6 +147,11 @@ export class TerminalInitialHintContribution extends Disposable implements ITerm } e.appendChild(this._hintWidget); e.classList.add('terminal-initial-hint'); + const font = this._xterm.getFont(); + if (font) { + e.style.fontFamily = font.fontFamily; + e.style.fontSize = font.fontSize + 'px'; + } } } if (this._hintWidget && this._xterm) { From 55d6200c43329b9e9498b81c8c1ca37c5ecddf3b Mon Sep 17 00:00:00 2001 From: David Dossett Date: Mon, 13 May 2024 10:41:31 -0700 Subject: [PATCH 139/357] Add `Codicon.attach` (#212624) --- .../browser/ui/codicons/codicon/codicon.ttf | Bin 80188 -> 80348 bytes src/vs/base/common/codiconsLibrary.ts | 1 + 2 files changed, 1 insertion(+) diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf index 807c949a277cad7db9d0a9c0834a5289da2c3155..eda8771f7a8096e97507c267a99897ee44fd1183 100644 GIT binary patch delta 7651 zcmXZh4SbGO9|!Q?f14S5G0ZkIGc()F`^*e848t(*^BQI|BW4)prEW=*Bq8-kl0*q9 zLRwmqBrQp$9!ZjP&izPI65YHGEH;;6M2RzMH1!mzVndDi87BBl-E*lKknD*4})(A0(!df4fyW zV|H*^L1gw3NbEtNe*9AvO@gZ+{45RVaq8w%-nLS;H%7vI_E!1%$=bYt(6x7@^j?O# z+4yq9$scVaCbo6ZEZ}{Rj$&udJ zCmrNzX(}ZWB4x;x4`hN&l__`%Y0?JE@EPu)yR5~0M95)0hvm{k8cLYdlUMLDwxL2c z%VIn(Psw~)F3V(rJSMB>FNHEmilkWXm+3N7 zX31=sBXeb;ERqN0L3v1)$iwo8JSv_QvQk#bGxDswC|hK!Y?JNss_c;0WT)(sH)OBu zlegrcye)^MLf)05@}V4)kL0+VluvO3f8Zu=VFL0|icXk`LU|H1FdNe`28l?L8h8S0 za7^B0FO0+_T$LCZE^8zihonFzdL&NfNptCl(ee&@N=>Z7`}jwqq!W^*nKVWRq$5Yx z$$r@*yXAH4k)e_&^)XF8mQUn_Y>?+s(tt3bt z36x$Kf@?BBX5irjES7Saj01QJ`{jM9L@ya3!!Qi*h@bf4J!yiv*tIt^I9NO!8r=V+ z3NRBC?_HRDh2NMHmEilY_GJ&54;0>u#wh9kbiu?%LB!gA&VN_sFKRMODHje8`F+l!Ud zV?Lxf5W*}`91>w3Rva8*mMS-8m`9XsX1cQ@ar=8z;c=!r5E9oyE6BqaacAQGk;Hvq zr4qN3CzZG}T&?6WrrQCD``a2NZXd3dh~qQNItAA%t}l?dlU=VkdSmUe&?h_%`2Hou zVH;+v;s6fwvf@w<<4%}3sKabm9NuAE0}%&$m{%2te3%`IgFnn`iqinhPQ?iUW*6Xb zFPs}-b}P;jFt01l7chGiXAYP*6z33_Hx*|Sn7xYg3d}ym83yJp#kmG%zv8R|b3k$a zVISmO=x{26d0TN(f;pr(J;A)AI8nj4%T1iJVBS@nykJ~Q6Q?nl_Y@~Im`cT|4d#7? zZpc|&m_1M`RCk_P5a#l;QG zO~qvn%q_(Q56oYROCOlq8=$x(gRP;sID-vTT&BSW zDK6MxgB6!%Q@K4yV(9`xlvnjjR#vtakU3qS8?438>YAdgsrEz7K9B~ zTouAbD6S7-BNbPQu=N$!jIdD(Y+>7gzi%6GXm(8)t;989LnW?7VwAWRX{5xpNURcf zwvCmzGmKN>&ajCRcY^Us+(|W6;&Dfupxm>JO;o~zvPnwZfi+X&4j@^HJAmd&+-F-T zaoJLd%T`KUwpQY@jl2GQk+ytukEC$>9%;wzdnA?H_egtg-y><CbfEC&^{@a(Mk^AUE93NCq)`D;dmmJ0=;zbUP*)#mrGMn(2OoWDL{&2+3IH044V^ z-4BwCXS&}YDP(%wkC04ax*s7aVh&YO%p9iVe&%o`)0ufnW->=8nZ+EbWH!?sEXf?E zJ6MvrOn0#4k%ioFM@zDZ>5i7<0p>U*4>H}6lY8j46O=4r<|}!aIZ??Y%mO8kGToVy ztYEqmC0WTVa@U_{E35csvXW<*?yO0kWlmA@B6F&eElk%MBwLx&lx$Sa*4dPnx{{m+=kpp%d0!E5Zcka>WNx*vAwfNnzbJBtD$NKCbwf3hS;X zf!EnSq4?+u>n<(vAr{uXCqB-?uD*-CgxAVGrTB~syGHS87j~^e5_6s66EN)4iqFEZ z>lL4hVO`%LxYlxghvdz*o1-Hml2eG-qyCd|HQnMPW44bywmOJgn=kgr3aT z6rbv0U2i2m-^1=weA0*At?&=?btO^mKHI}L#6ke9>(T^wHg76v#&q4ASSEmV-J4i2 zfOXxQSUP~+uaLt$pu}C9gG$`LyKYb7e$%zmA=ho)k-0M>)*WEoJwR|Da<>4l3h*3e%YGOi4aw9#QfM^8+O(m`9atV0u2}3yJFku6K~QKJbwe*9Y9aMB@5@ z+cAmj18!%;atN&Z5n^Ek_N0P4=uZ`kC9qWrZl9ki4=X6Jrxa@`u%9bdRbWpm)>mM^ zP^`4To>8p1z<#M%eS!T-u?_?KwPHmE_N-!U2KF1pDh=$n?)rbrqjt^ic1A4Sz`7qH z7I9$Rj}wZS=M@V(u-_||cwjFm7JFcSP%QhvUQ{dq!TzY=F2yA!iM;;yCxuw%WyL}f z?9YlNBiJhnVa#8YxK3si3ret7v9tv1RV*^Wy7$C#6YN#R!V~PT>nr(z`w_NIbsom-04E!e*l*dM~}ZN4FvykP&U zuwre`#+^MpH?Js`z~J>!EQZ1Bt5_C;*H3|6)>}=9OMk_p8NAgM%V+QgC>GM-t)W;_ zgSVz)aSh%;4o&WbbvAf|6!tQM6>D$shA8xB)>4ws3{|Ym!CPD56tj*Jx4*hdUSjg1 zI;`Nq%P#7mH@l{nSJh!z58eoc?aWBUQXjnT$O&zk?u3hzgtw6bJAyaXVb>Jb6zqx)tBCN%DLl+)Ojc6K^fc!S!5wJ}B_o(E6|0xMh4RBL)}pR8N%Hu{_-6R7^1b30<~P;vRJGV@CH!w=wX6O${dZI! zSAA1JQb2aV?i#T*&emL6^K{MIfkA=80*eC616KrY47?P0Cnz*1I;c}nQP8@eTfwb@ zvx27vuL(g&O331nLm_8sCD&S7>tv{ZXmaTO(2KPLY8TbsT>D0yNc~Rr z``2Gq|5OyBqN1{*#zmDyZHqb{#(UqMTbl20qM!< zebS54m!@w_Ka_qt{aVL>j&S$ zL0uMg+0o@%W_V^=W^QJ2=DN(v%*$QFyUy*py6fd`(cOx>t?5?P-M@Rbr~88LC%XTg zm7GN%@dUawWXDzg)^bF!b!KHIxf?=`)v`h@iQhE zS>B<%8zVADY#dQF!aFi)Ye+#L|h|3Ni{x3pN&9EZk6dbW+br zizk_)Iz|19j(CdhOwO9TbaG{}fANsw9mQ2sBB$(`8Za$j`r+xOB&1|P$(E9;lFKu~ zW)#g>H{){Y($d4FRi)=kua@4KId0~rndfIE%_^C7es;j@yxHf<%FA9UyFI7HoZ>l$ z=VEUExzpyJpBFc;aNgm0=gXtZ`;>1fubjVh{*49e7ltexvTzme|GiHnR#fxwuH-%J z!)gNuj1FPpDP1KkB|NN__{SxrayvC4j@x(to+<59TS=2RfBqrezq)T;NMeKF!fK&` z{sq2#TM$?;Hn5;Z&DxD>2S?V-^Ih-j&yPeW@*{z@_>q8WK81me!UGFxMh4f3sa=B~ q;X(9i`PX%+I*=7B8+?6yK6~a|6ZxaMe|}k6enIhp&*Nok*#80V)nlXp delta 7529 zcmXZg3w+Jx9|!QycbnU8hS`{HW@hF-48t(YFbp&IVRM@#IZKkXgsD`L(2^vf zNLsofp^{3Hq>`TJq*6)p*Z=eQ#_N4P=bWAMJHOxadw##?dA=8Z_d9&uZ*8KdTiaO? zK#dq6;^V0^7v-mZUGoIcv<$ehsbEUp2I~$t4mt(lXKCr4ZN=-nZROOyXbJTxA5%S~{EqzkUl#qBAujh_ z^PA~ge%i0ze{b$AkE>CqL)_gB!7m!Vcb~On`U6$s;OBdtKT#VYe8oE$_9y(SrR1kFi75!2{KIzuofMp4erNj{12(J9wi8q zH}NDMkX{lgbtF`t$8kJ|H{==dV70835_v%GmqoHl9+A~@AKsK@QdS<~AMS}jB%%S(t-)n2!Z0#zHK@Vl2T@+=FE(#l5%>E3gu)Ja`Ce@CeqU92@W`9>e3< zh$rwAp2lW8gDrR#Td^I_V+UT~XLjO6yo3Wd$X@Uk-o<-3h7WK8AL1m=;8UE%XZRdn z;2gfhHT;I__!BqrH*Vn{{40T17wh7%3v8HLuHhVmN7C`a%8+rlF2ef zrpk1gDMd0z=E{6oAd6**ER}ntRPL4KvO-qMgYu9^9+t=CaoHlz$~JjUDrASeAUown z*(G~quk4fkazGBs>+*&imUrbnd0&poG5JtV;&=Rk8~6(okc-*qj3P{uH7LYf%)}VP zqp4I!8P?%_d4-)Y9Fy_0L`k-+lV*5bCQ6<(5>K%-mt>5VL+C9v@Gy?xj?|aVXeJ2~ zgO2Eg{!%Wl%FFVS?8eKICBu0^&5+}ALOzfuf&v@Bk>+IL=0}Dj=Uyy;l#GI+*9j5yd$pmJh5`N#zR=kzK6e-?RVCE=vV9r&% z(ZIOhAb9TQllh8wAD9J-_aT^Kg;eH3#d{M>iQ-)fW|2Y|bFt!`3ucMpJq%{4!UN2E zl=NaQi9-GK@R_1lKB$ zD&aw!#~eR?p{)Ew$RoUM!)#N$fx~Q9yp_YaVfk*Eyz$=Qg2$&xg=Mykj z6=xPOKPk>1V2t8y1I8-OJ7BzuGZ2`c73U%_*A!Y`I9GJfpCp<7W6{kKhe=AOcU~Z9z51bFd{G;w3*=;^2 z&XHjLr#M@JxuZC5g85f*2E~C@aV`Zb%FV5m<+1?#6c=Yq#p;S1vQ3%07_ zL=3i?;*<=wy5i&vwua&~4K_e=!Uh|tICX;!Qk=xW1}jeIU~4K)>|jIgV*7u=C$$u3 zd$6??=Y6ns6lZ|2p^9@s*t&|dLfA0H`5|n$;!F{?p5mMlw!Y%*5jH|`9tqokzi%7x z=-He$Qn3YLqm;PTiB{rTr=b$pIx$MzkvCG}jy6__JKDxd+|kAHGC>J<&^A-zcBZ)!w=*r2xbL-8;@)ee#J$&AiF>b&68B!BJO6wTvTc>PY^TI! zdnGQDl(_7m#AQb%U6`GebY&(hNnv(Y(v9gpN79|wzjYrZ>A`d#BuQnu50dm`c30Ay z={`r&hv_~?(wFHzN79ekOGyUPeTQTKGfl}Lru%*$5C4$KC+SKCGu_5WhA`d6NQN@| zD;dRf8zvdebQ>la!yKq&EYtl6NeN|rL+-jdwIoS>wX znXBBQ+~z4+&YY-Z1=AfJ$x5a>IFbjM?%+rsV&=Q^pU)Q$^MyN7lE;_@N*-rUQ?iBW zT7cwP<_smP+pP+SYcKB~AXhIRdgxITt;{e|FKYop?t8TJX~N;vjO#dS37Q?5y$;uqLE z?IwkvnNKT;Vs2KF&3s0Q`>`#Gi*Q)iAqnmrZ&l*1hHXmnnA?>!V!Dn>T(@T>Qhnq;Q0}Tj37#WhM2Q?ARXnz$yW( zYbJs_8rRE75}2-+lf*DxFDF(LU|&_NE5N>{(4Togi92ORgO(j{(N+rXYZz)#|v_}+XFyB^kocWHD6U=v&e86;96v-1z*GJw@ zp*M_(-vq0{gLI zRR#7F#rg{D8O2Hq?5Bz~7ud6k)fd>$6zedspDR{mV83uDkq<1+z@Aeq)4+bISg?V0 z8z7c$VBLm^MI2c7gT!(U?AHoYnHLpHJh0y=7JFbXDVBX;zf~*%@g!d63t}w@_B#c4 z624dBF6JK;3q`P36k?b^Di)7muPW4G{-nh9E~8jlg0+f8CfK_?{9(8EKPy(AV6Q3G zpkRMdtVY59s#uqT{Y|k#1$$kwRt5XJV$}-v55@Ww?4Qa_xa|!E*CL(&;se1o)lJ1h z7wq2(u5a8@EPlcMqwrvPMx(AC9;a6ni(&BkD3-<6Ymm5p(pItF32!?E*K+L@YoPdzeZ+CqQ2yJ%724JE;J9dy&u!mK-yObZ{hIr& z^}AH1HUH;S*W!~=y56n&f%VhtudRQi{`m->i0Fu2b-rhU)}t4i-|3^wm8=^xaGi>*IE^{Dr+6zdPnOkZ4%q0wOQR}cboHx z5s9gZn-fpBO>8@}?UQXUwM%VR)NW0?3++SNC$;x%ZGSTZ&vZ2@{wXObTT&{!g>*~lHoe=PZl8C%(LK0(Lig(#5*%3iyBC-pAueKoCJpRhieeMANA}b^7{IH~9YloR^PwniE*<-Tz zW}nHvF+5{<#qdibVn(EoSUh6?h&v;5My?sTYvk2Y^G97AJ!*92nCLMZ#+)2mb!_g~ zvpEqtE5`+oJ3H?B_>l2;CL~TspD=U6YZGqgM&@SbuFs3jOUql9SDANtV(`SoiTM+6 zPfDDWGimGOF_TwJ@tcx5W#^PL`GNV%^D8}5eWs>OEuDI_pmxFZf;9!_rWH*)KI8bT zWwUn9I$hYMaAM(_!ivJPv*TxH&#ov+D_T~xrf74~?xM;$(R0Sn**xd!+}3k9&pkdb zY~H5%$@2^5ADZu7kg#Ccf+NL&#Vw0B7hhf&w{Y3Q%?mG-R4HBzbocwuUZWEavD)>JHgkY-x diff --git a/src/vs/base/common/codiconsLibrary.ts b/src/vs/base/common/codiconsLibrary.ts index 03118140340..599873fbacf 100644 --- a/src/vs/base/common/codiconsLibrary.ts +++ b/src/vs/base/common/codiconsLibrary.ts @@ -578,4 +578,5 @@ export const codiconsLibrary = { goToSearch: register('go-to-search', 0xec32), percentage: register('percentage', 0xec33), sortPercentage: register('sort-percentage', 0xec33), + attach: register('attach', 0xec34), } as const; From 8a04a3352dbf9d95f3f53cee36faad8e741fc9d1 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Mon, 13 May 2024 10:55:59 -0700 Subject: [PATCH 140/357] provide screen reader users with correct info for suggested question (#212612) fix #212323 --- src/vs/workbench/contrib/chat/browser/chatFollowups.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatFollowups.ts b/src/vs/workbench/contrib/chat/browser/chatFollowups.ts index 718c863904a..b4ee434a778 100644 --- a/src/vs/workbench/contrib/chat/browser/chatFollowups.ts +++ b/src/vs/workbench/contrib/chat/browser/chatFollowups.ts @@ -69,7 +69,7 @@ export class ChatFollowups extend } else if (followup.kind === 'command') { button.element.classList.add('interactive-followup-command'); } - button.element.ariaLabel = localize('followUpAriaLabel', "Follow up question: {0}", followup.title); + button.element.ariaLabel = localize('followUpAriaLabel', "Follow up question: {0}", baseTitle); button.label = new MarkdownString(baseTitle); this._register(button.onDidClick(() => this.clickHandler(followup))); From 501b2bb22c94fec0c8cc0eda56d3cacae8ede584 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 13 May 2024 20:12:43 +0200 Subject: [PATCH 141/357] add source context to extension installation (#212625) --- .../extensionManagement/common/extensionManagement.ts | 6 +++++- src/vs/platform/userDataSync/common/extensionsSync.ts | 4 ++-- .../contrib/extensions/browser/extensions.contribution.ts | 6 +++--- .../common/extensionManagementService.ts | 7 ++++--- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 9ae308b2c9e..902d2d9bee9 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -17,11 +17,15 @@ export const EXTENSION_IDENTIFIER_PATTERN = '^([a-z0-9A-Z][a-z0-9-A-Z]*)\\.([a-z export const EXTENSION_IDENTIFIER_REGEX = new RegExp(EXTENSION_IDENTIFIER_PATTERN); export const WEB_EXTENSION_TAG = '__web_extension'; export const EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT = 'skipWalkthrough'; -export const EXTENSION_INSTALL_SYNC_CONTEXT = 'extensionsSync'; export const EXTENSION_INSTALL_SOURCE_CONTEXT = 'extensionInstallSource'; export const EXTENSION_INSTALL_DEP_PACK_CONTEXT = 'dependecyOrPackExtensionInstall'; export const EXTENSION_INSTALL_CLIENT_TARGET_PLATFORM_CONTEXT = 'clientTargetPlatform'; +export const enum ExtensionInstallSource { + COMMAND = 'command', + SETTINGS_SYNC = 'settingsSync', +} + export interface IProductVersion { readonly version: string; readonly date?: string; diff --git a/src/vs/platform/userDataSync/common/extensionsSync.ts b/src/vs/platform/userDataSync/common/extensionsSync.ts index a490da8344a..367a89bf201 100644 --- a/src/vs/platform/userDataSync/common/extensionsSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsSync.ts @@ -15,7 +15,7 @@ import { URI } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { GlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionEnablementService'; -import { IExtensionGalleryService, IExtensionManagementService, IGlobalExtensionEnablementService, ILocalExtension, ExtensionManagementError, ExtensionManagementErrorCode, IGalleryExtension, DISABLED_EXTENSIONS_STORAGE_PATH, EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT, EXTENSION_INSTALL_SYNC_CONTEXT, InstallExtensionInfo } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionGalleryService, IExtensionManagementService, IGlobalExtensionEnablementService, ILocalExtension, ExtensionManagementError, ExtensionManagementErrorCode, IGalleryExtension, DISABLED_EXTENSIONS_STORAGE_PATH, EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT, EXTENSION_INSTALL_SOURCE_CONTEXT, InstallExtensionInfo, ExtensionInstallSource } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionStorageService, IExtensionStorageService } from 'vs/platform/extensionManagement/common/extensionStorage'; import { ExtensionType, IExtensionIdentifier, isApplicationScopedExtension } from 'vs/platform/extensions/common/extensions'; @@ -487,7 +487,7 @@ export class LocalExtensionsProvider { installPreReleaseVersion: e.preRelease, profileLocation: profile.extensionsResource, isApplicationScoped: e.isApplicationScoped, - context: { [EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT]: true, [EXTENSION_INSTALL_SYNC_CONTEXT]: true } + context: { [EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT]: true, [EXTENSION_INSTALL_SOURCE_CONTEXT]: ExtensionInstallSource.SETTINGS_SYNC } } }); syncExtensionsToInstall.set(extension.identifier.id.toLowerCase(), e); diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index c9c19453de6..71b704376e4 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -8,7 +8,7 @@ import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { Registry } from 'vs/platform/registry/common/platform'; import { MenuRegistry, MenuId, registerAction2, Action2, ISubmenuItem, IMenuItem, IAction2Options } from 'vs/platform/actions/common/actions'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { ExtensionsLocalizedLabel, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel, InstallOperation, EXTENSION_INSTALL_SOURCE_CONTEXT } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionsLocalizedLabel, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel, InstallOperation, EXTENSION_INSTALL_SOURCE_CONTEXT, ExtensionInstallSource } from 'vs/platform/extensionManagement/common/extensionManagement'; import { EnablementState, IExtensionManagementServerService, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService, extensionsConfigurationNodeBase } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; @@ -365,13 +365,13 @@ CommandsRegistry.registerCommand({ isMachineScoped: options?.donotSync ? true : undefined, /* do not allow syncing extensions automatically while installing through the command */ installPreReleaseVersion: options?.installPreReleaseVersion, installGivenVersion: !!version, - context: { ...options?.context, [EXTENSION_INSTALL_SOURCE_CONTEXT]: 'command' }, + context: { ...options?.context, [EXTENSION_INSTALL_SOURCE_CONTEXT]: ExtensionInstallSource.COMMAND }, }); } else { await extensionsWorkbenchService.install(arg, { version, installPreReleaseVersion: options?.installPreReleaseVersion, - context: { ...options?.context, [EXTENSION_INSTALL_SOURCE_CONTEXT]: 'command' }, + context: { ...options?.context, [EXTENSION_INSTALL_SOURCE_CONTEXT]: ExtensionInstallSource.COMMAND }, justification: options?.justification, enable: options?.enable, isMachineScoped: options?.donotSync ? true : undefined, /* do not allow syncing extensions automatically while installing through the command */ diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index 9b961506462..2bbfd336f4d 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -5,8 +5,9 @@ import { Emitter, Event, EventMultiplexer } from 'vs/base/common/event'; import { - ILocalExtension, IGalleryExtension, IExtensionIdentifier, IExtensionsControlManifest, IExtensionGalleryService, InstallOptions, UninstallOptions, InstallExtensionResult, ExtensionManagementError, ExtensionManagementErrorCode, Metadata, InstallOperation, EXTENSION_INSTALL_SYNC_CONTEXT, InstallExtensionInfo, - IProductVersion + ILocalExtension, IGalleryExtension, IExtensionIdentifier, IExtensionsControlManifest, IExtensionGalleryService, InstallOptions, UninstallOptions, InstallExtensionResult, ExtensionManagementError, ExtensionManagementErrorCode, Metadata, InstallOperation, EXTENSION_INSTALL_SOURCE_CONTEXT, InstallExtensionInfo, + IProductVersion, + ExtensionInstallSource } from 'vs/platform/extensionManagement/common/extensionManagement'; import { DidChangeProfileForServerEvent, DidUninstallExtensionOnServerEvent, IExtensionManagementServer, IExtensionManagementServerService, InstallExtensionOnServerEvent, IResourceExtension, IWorkbenchExtensionManagementService, UninstallExtensionOnServerEvent } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ExtensionType, isLanguagePackExtension, IExtensionManifest, getWorkspaceSupportTypeMessage, TargetPlatform } from 'vs/platform/extensions/common/extensions'; @@ -566,7 +567,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench throw error; } - if (!installOptions?.context?.[EXTENSION_INSTALL_SYNC_CONTEXT]) { + if (installOptions?.context?.[EXTENSION_INSTALL_SOURCE_CONTEXT] !== ExtensionInstallSource.SETTINGS_SYNC) { await this.checkForWorkspaceTrust(manifest, false); } From 929bec07f137ffd7c708790947e5c8a4074fe26d Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Mon, 13 May 2024 11:35:03 -0700 Subject: [PATCH 142/357] eng: make it selfhost failure log easier to access and more stable (#212626) --- .../package.json | 7 +++++ .../src/extension.ts | 11 ++++---- .../src/failureTracker.ts | 26 ++++++++++++++++--- 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/.vscode/extensions/vscode-selfhost-test-provider/package.json b/.vscode/extensions/vscode-selfhost-test-provider/package.json index 3b019f34ec9..f472098cd14 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/package.json +++ b/.vscode/extensions/vscode-selfhost-test-provider/package.json @@ -14,6 +14,13 @@ { "command": "selfhost-test-provider.updateSnapshot", "title": "Update Snapshot", + "category": "Testing", + "icon": "$(merge)" + }, + { + "command": "selfhost-test-provider.openFailureLog", + "title": "Open Selfhost Failure Logs", + "category": "Testing", "icon": "$(merge)" } ], diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts index f68e7f99e89..e1b2fd8b51e 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts @@ -55,7 +55,11 @@ export async function activate(context: vscode.ExtensionContext) { } }; - let startedTrackingFailures = false; + guessWorkspaceFolder().then(folder => { + if (folder) { + context.subscriptions.push(new FailureTracker(context, folder.uri.fsPath)); + } + }); const createRunHandler = ( runnerCtor: { new (folder: vscode.WorkspaceFolder): VSCodeTestRunner }, @@ -71,11 +75,6 @@ export async function activate(context: vscode.ExtensionContext) { return; } - if (!startedTrackingFailures) { - startedTrackingFailures = true; - context.subscriptions.push(new FailureTracker(folder.uri.fsPath)); - } - const runner = new runnerCtor(folder); const map = await getPendingTestMap(ctrl, req.include ?? gatherTestItems(ctrl.items)); const task = ctrl.createTestRun(req); diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/failureTracker.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/failureTracker.ts index e232fa133e3..e04d4beede4 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/failureTracker.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/failureTracker.ts @@ -4,8 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { spawn } from 'child_process'; +import { existsSync, mkdirSync, renameSync } from 'fs'; import { readFile, writeFile } from 'fs/promises'; -import { join } from 'path'; +import { dirname, join } from 'path'; import * as vscode from 'vscode'; interface IGitState { @@ -29,10 +30,29 @@ export class FailureTracker { { snapshot: vscode.TestResultSnapshot; failing: IGitState } >(); - private readonly logFile = join(this.rootDir, '.build/vscode-test-failures.json'); + private readonly logFile: string; private logs?: ITrackedRemediation[]; - constructor(private readonly rootDir: string) { + constructor(context: vscode.ExtensionContext, private readonly rootDir: string) { + this.logFile = join(context.globalStorageUri.fsPath, '.build/vscode-test-failures.json'); + mkdirSync(dirname(this.logFile), { recursive: true }); + + const oldLogFile = join(rootDir, '.build/vscode-test-failures.json'); + if (existsSync(oldLogFile)) { + try { + renameSync(oldLogFile, this.logFile); + } catch { + // ignore + } + } + + this.disposables.push( + vscode.commands.registerCommand('selfhost-test-provider.openFailureLog', async () => { + const doc = await vscode.workspace.openTextDocument(this.logFile); + await vscode.window.showTextDocument(doc); + }) + ); + this.disposables.push( vscode.tests.onDidChangeTestResults(() => { const last = vscode.tests.testResults[0]; From 90edab144f0cfcd6d7f40ac55f1542cc010f5e74 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Mon, 13 May 2024 11:49:47 -0700 Subject: [PATCH 143/357] re-enable terminal chat hint by default, fix bug (#212630) fix #212221 --- .../browser/terminal.initialHint.contribution.ts | 13 ++++++++++++- .../chat/common/terminalInitialHintConfiguration.ts | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts index 8fc93f1577c..90c223f507f 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts @@ -102,7 +102,8 @@ export class TerminalInitialHintContribution extends Disposable implements ITerm private _createHint(): void { const instance = this._instance instanceof TerminalInstance ? this._instance : undefined; - if (!instance || !this._xterm || this._hintWidget || instance?.capabilities.get(TerminalCapability.CommandDetection)?.hasInput) { + const commandDetectionCapability = instance?.capabilities.get(TerminalCapability.CommandDetection); + if (!instance || !this._xterm || this._hintWidget || !commandDetectionCapability || !commandDetectionCapability?.hasInput) { return; } @@ -131,6 +132,16 @@ export class TerminalInitialHintContribution extends Disposable implements ITerm this._addon?.dispose(); })); + const inputModel = commandDetectionCapability.promptInputModel; + if (inputModel) { + this._register(inputModel.onDidChangeInput(() => { + if (inputModel.value) { + this._decoration?.dispose(); + this._addon?.dispose(); + } + })); + } + if (!this._decoration) { return; } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/common/terminalInitialHintConfiguration.ts b/src/vs/workbench/contrib/terminalContrib/chat/common/terminalInitialHintConfiguration.ts index 618f80e1967..41567ee5921 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/common/terminalInitialHintConfiguration.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/common/terminalInitialHintConfiguration.ts @@ -16,6 +16,6 @@ export const terminalInitialHintConfiguration: IStringDictionary Date: Mon, 13 May 2024 21:06:02 +0200 Subject: [PATCH 144/357] Tabs Multi Select (#211712) * Tabs Multi Select v1 * Color * Only rerender selections * Improve drag and drop and tab border top drawing * Improved multi select behaviour * Open With Editor multiple "support" * :lipstick: * tests * Move down to model * Fix tests * Sync selection and active in model * Make unselect async * async unselect in interface * Model update event when unselecting with closeEditor * async fir selectEditor * Fix tests and :lipstick: --- .../lib/stylelint/vscode-known-variables.json | 3 + .../contrib/clipboard/browser/clipboard.ts | 1 - .../parts/editor/editor.contribution.ts | 9 +- .../workbench/browser/parts/editor/editor.ts | 5 + .../browser/parts/editor/editorActions.ts | 33 ++- .../browser/parts/editor/editorCommands.ts | 202 ++++++++++------ .../browser/parts/editor/editorDropTarget.ts | 25 +- .../browser/parts/editor/editorGroupView.ts | 78 +++++- .../browser/parts/editor/editorTabsControl.ts | 3 + .../parts/editor/editorTitleControl.ts | 4 + .../editor/media/multieditortabscontrol.css | 9 +- .../parts/editor/multiEditorTabsControl.ts | 225 +++++++++++++----- .../parts/editor/multiRowEditorTabsControl.ts | 8 + .../parts/editor/noEditorTabsControl.ts | 2 + .../parts/editor/singleEditorTabsControl.ts | 2 + src/vs/workbench/common/contextkeys.ts | 2 + src/vs/workbench/common/editor.ts | 1 + .../common/editor/editorGroupModel.ts | 122 +++++++++- .../common/editor/filteredEditorGroupModel.ts | 2 + src/vs/workbench/common/theme.ts | 22 ++ .../browser/externalTerminal.contribution.ts | 3 +- .../files/browser/fileActions.contribution.ts | 22 +- .../contrib/files/browser/fileCommands.ts | 12 +- .../workbench/contrib/files/browser/files.ts | 11 +- .../fileActions.contribution.ts | 5 +- .../search/browser/searchActionsFind.ts | 3 +- .../editor/common/editorGroupsService.ts | 34 +++ .../test/browser/editorGroupsService.test.ts | 67 ++++++ .../test/browser/workbenchTestServices.ts | 6 + 29 files changed, 734 insertions(+), 187 deletions(-) diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index 497957a39c5..e6291132d56 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -647,6 +647,9 @@ "--vscode-tab-activeBackground", "--vscode-tab-activeBorder", "--vscode-tab-activeBorderTop", + "--vscode-tab-selectedBorderTop", + "--vscode-tab-selectedBackground", + "--vscode-tab-selectedForeground", "--vscode-tab-activeForeground", "--vscode-tab-activeModifiedBorder", "--vscode-tab-border", diff --git a/src/vs/editor/contrib/clipboard/browser/clipboard.ts b/src/vs/editor/contrib/clipboard/browser/clipboard.ts index b18e3dd0b81..e10b43e1f37 100644 --- a/src/vs/editor/contrib/clipboard/browser/clipboard.ts +++ b/src/vs/editor/contrib/clipboard/browser/clipboard.ts @@ -111,7 +111,6 @@ export const CopyAction = supportsCopy ? registerCommand(new MultiCommand({ MenuRegistry.appendMenuItem(MenuId.MenubarEditMenu, { submenu: MenuId.MenubarCopy, title: nls.localize2('copy as', "Copy As"), group: '2_ccp', order: 3 }); MenuRegistry.appendMenuItem(MenuId.EditorContext, { submenu: MenuId.EditorContextCopy, title: nls.localize2('copy as', "Copy As"), group: CLIPBOARD_CONTEXT_MENU_GROUP, order: 3 }); MenuRegistry.appendMenuItem(MenuId.EditorContext, { submenu: MenuId.EditorContextShare, title: nls.localize2('share', "Share"), group: '11_share', order: -1, when: ContextKeyExpr.and(ContextKeyExpr.notEquals('resourceScheme', 'output'), EditorContextKeys.editorTextFocus) }); -MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { submenu: MenuId.EditorTitleContextShare, title: nls.localize2('share', "Share"), group: '11_share', order: -1 }); MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { submenu: MenuId.ExplorerContextShare, title: nls.localize2('share', "Share"), group: '11_share', order: -1 }); export const PasteAction = supportsPaste ? registerCommand(new MultiCommand({ diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index 1611076ea51..432513e4157 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -11,7 +11,7 @@ import { TextCompareEditorActiveContext, ActiveEditorPinnedContext, EditorGroupEditorsCountContext, ActiveEditorStickyContext, ActiveEditorAvailableEditorIdsContext, EditorPartMultipleEditorGroupsContext, ActiveEditorDirtyContext, ActiveEditorGroupLockedContext, ActiveEditorCanSplitInGroupContext, SideBySideEditorActiveContext, EditorTabsVisibleContext, ActiveEditorLastInGroupContext, EditorPartMaximizedEditorGroupContext, MultipleEditorGroupsContext, InEditorZenModeContext, - IsAuxiliaryEditorPartContext, ActiveCompareEditorCanSwapContext + IsAuxiliaryEditorPartContext, ActiveCompareEditorCanSwapContext, MultipleEditorsSelectedContext } from 'vs/workbench/common/contextkeys'; import { SideBySideEditorInput, SideBySideEditorInputSerializer } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { TextResourceEditor } from 'vs/workbench/browser/parts/editor/textResourceEditor'; @@ -388,7 +388,7 @@ MenuRegistry.appendMenuItem(MenuId.EditorActionsPositionSubmenu, { command: { id // Editor Title Context Menu MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: CLOSE_EDITOR_COMMAND_ID, title: localize('close', "Close") }, group: '1_close', order: 10 }); MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: CLOSE_OTHER_EDITORS_IN_GROUP_COMMAND_ID, title: localize('closeOthers', "Close Others"), precondition: EditorGroupEditorsCountContext.notEqualsTo('1') }, group: '1_close', order: 20 }); -MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: CLOSE_EDITORS_TO_THE_RIGHT_COMMAND_ID, title: localize('closeRight', "Close to the Right"), precondition: ActiveEditorLastInGroupContext.toNegated() }, group: '1_close', order: 30, when: EditorTabsVisibleContext }); +MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: CLOSE_EDITORS_TO_THE_RIGHT_COMMAND_ID, title: localize('closeRight', "Close to the Right"), precondition: ContextKeyExpr.and(ActiveEditorLastInGroupContext.toNegated(), MultipleEditorsSelectedContext.negate()) }, group: '1_close', order: 30, when: EditorTabsVisibleContext }); MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: CLOSE_SAVED_EDITORS_COMMAND_ID, title: localize('closeAllSaved', "Close Saved") }, group: '1_close', order: 40 }); MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: CLOSE_EDITORS_IN_GROUP_COMMAND_ID, title: localize('closeAll', "Close All") }, group: '1_close', order: 50 }); MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: REOPEN_WITH_COMMAND_ID, title: localize('reopenWith', "Reopen Editor With...") }, group: '1_open', order: 10, when: ActiveEditorAvailableEditorIdsContext }); @@ -399,10 +399,11 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: SPLIT_ED MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: SPLIT_EDITOR_DOWN, title: localize('splitDown', "Split Down") }, group: '5_split', order: 20 }); MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: SPLIT_EDITOR_LEFT, title: localize('splitLeft', "Split Left") }, group: '5_split', order: 30 }); MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: SPLIT_EDITOR_RIGHT, title: localize('splitRight', "Split Right") }, group: '5_split', order: 40 }); -MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: SPLIT_EDITOR_IN_GROUP, title: localize('splitInGroup', "Split in Group") }, group: '6_split_in_group', order: 10, when: ActiveEditorCanSplitInGroupContext }); -MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: JOIN_EDITOR_IN_GROUP, title: localize('joinInGroup', "Join in Group") }, group: '6_split_in_group', order: 10, when: SideBySideEditorActiveContext }); +MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: SPLIT_EDITOR_IN_GROUP, title: localize('splitInGroup', "Split in Group"), precondition: MultipleEditorsSelectedContext.negate() }, group: '6_split_in_group', order: 10, when: ActiveEditorCanSplitInGroupContext }); +MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: JOIN_EDITOR_IN_GROUP, title: localize('joinInGroup', "Join in Group"), precondition: MultipleEditorsSelectedContext.negate() }, group: '6_split_in_group', order: 10, when: SideBySideEditorActiveContext }); MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: MOVE_EDITOR_INTO_NEW_WINDOW_COMMAND_ID, title: localize('moveToNewWindow', "Move into New Window") }, group: '7_new_window', order: 10 }); MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: COPY_EDITOR_INTO_NEW_WINDOW_COMMAND_ID, title: localize('copyToNewWindow', "Copy into New Window") }, group: '7_new_window', order: 20 }); +MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { submenu: MenuId.EditorTitleContextShare, title: localize('share', "Share"), group: '11_share', order: -1, when: MultipleEditorsSelectedContext.negate() }); // Editor Title Menu MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: TOGGLE_DIFF_SIDE_BY_SIDE, title: localize('inlineView', "Inline View"), toggled: ContextKeyExpr.equals('config.diffEditor.renderSideBySide', false) }, group: '1_diff', order: 10, when: ContextKeyExpr.has('isInDiffEditor') }); diff --git a/src/vs/workbench/browser/parts/editor/editor.ts b/src/vs/workbench/browser/parts/editor/editor.ts index ead5ace8b41..f2a1a929bde 100644 --- a/src/vs/workbench/browser/parts/editor/editor.ts +++ b/src/vs/workbench/browser/parts/editor/editor.ts @@ -333,6 +333,11 @@ export interface IInternalEditorOpenOptions extends IInternalEditorTitleControlO * the top that the editor opens in. */ readonly preserveWindowOrder?: boolean; + + /** + * Whether to add the editor to the selection or not. + */ + readonly selected?: boolean; } export interface IInternalEditorCloseOptions extends IInternalEditorTitleControlOptions { diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index 921a28459cc..703a512413f 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -13,7 +13,7 @@ import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/bro import { GoFilter, IHistoryService } from 'vs/workbench/services/history/common/history'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { CLOSE_EDITOR_COMMAND_ID, MOVE_ACTIVE_EDITOR_COMMAND_ID, ActiveEditorMoveCopyArguments, SPLIT_EDITOR_LEFT, SPLIT_EDITOR_RIGHT, SPLIT_EDITOR_UP, SPLIT_EDITOR_DOWN, splitEditor, LAYOUT_EDITOR_GROUPS_COMMAND_ID, UNPIN_EDITOR_COMMAND_ID, COPY_ACTIVE_EDITOR_COMMAND_ID, SPLIT_EDITOR, resolveCommandsContext, getCommandsContext, TOGGLE_MAXIMIZE_EDITOR_GROUP, MOVE_EDITOR_INTO_NEW_WINDOW_COMMAND_ID, COPY_EDITOR_INTO_NEW_WINDOW_COMMAND_ID, MOVE_EDITOR_GROUP_INTO_NEW_WINDOW_COMMAND_ID, COPY_EDITOR_GROUP_INTO_NEW_WINDOW_COMMAND_ID, NEW_EMPTY_EDITOR_WINDOW_COMMAND_ID as NEW_EMPTY_EDITOR_WINDOW_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; +import { CLOSE_EDITOR_COMMAND_ID, MOVE_ACTIVE_EDITOR_COMMAND_ID, ActiveEditorMoveCopyArguments, SPLIT_EDITOR_LEFT, SPLIT_EDITOR_RIGHT, SPLIT_EDITOR_UP, SPLIT_EDITOR_DOWN, splitEditor, LAYOUT_EDITOR_GROUPS_COMMAND_ID, UNPIN_EDITOR_COMMAND_ID, COPY_ACTIVE_EDITOR_COMMAND_ID, SPLIT_EDITOR, resolveCommandsContext, getCommandsContext, TOGGLE_MAXIMIZE_EDITOR_GROUP, MOVE_EDITOR_INTO_NEW_WINDOW_COMMAND_ID, COPY_EDITOR_INTO_NEW_WINDOW_COMMAND_ID, MOVE_EDITOR_GROUP_INTO_NEW_WINDOW_COMMAND_ID, COPY_EDITOR_GROUP_INTO_NEW_WINDOW_COMMAND_ID, NEW_EMPTY_EDITOR_WINDOW_COMMAND_ID as NEW_EMPTY_EDITOR_WINDOW_COMMAND_ID, getEditorsFromContext } from 'vs/workbench/browser/parts/editor/editorCommands'; import { IEditorGroupsService, IEditorGroup, GroupsArrangement, GroupLocation, GroupDirection, preferredSideBySideGroupDirection, IFindGroupScope, GroupOrientation, EditorGroupLayout, GroupsOrder, MergeGroupMode } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -65,7 +65,7 @@ abstract class AbstractSplitEditorAction extends Action2 { const editorGroupService = accessor.get(IEditorGroupsService); const configurationService = accessor.get(IConfigurationService); - splitEditor(editorGroupService, this.getDirection(configurationService), context); + splitEditor(editorGroupService, this.getDirection(configurationService), [context]); } } @@ -2514,19 +2514,26 @@ abstract class BaseMoveCopyEditorToNewWindowAction extends Action2 { override async run(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) { const editorGroupService = accessor.get(IEditorGroupsService); + const editors = getEditorsFromContext(accessor, resourceOrContext, context); - const { group, editor } = resolveCommandsContext(editorGroupService, getCommandsContext(resourceOrContext, context)); - if (group && editor) { - const auxiliaryEditorPart = await editorGroupService.createAuxiliaryEditorPart(); - - if (this.move) { - group.moveEditor(editor, auxiliaryEditorPart.activeGroup); - } else { - group.copyEditor(editor, auxiliaryEditorPart.activeGroup); - } - - auxiliaryEditorPart.activeGroup.focus(); + // If there is no editor, do not create a new window + if (editors.length === 0) { + return; } + + const auxiliaryEditorPart = await editorGroupService.createAuxiliaryEditorPart(); + + for (const { editor, group } of editors) { + if (group && editor) { + if (this.move) { + group.moveEditor(editor, auxiliaryEditorPart.activeGroup); + } else { + group.copyEditor(editor, auxiliaryEditorPart.activeGroup); + } + } + } + + auxiliaryEditorPart.activeGroup.focus(); } } diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index f704886061d..b38ddb63f17 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -36,7 +36,7 @@ import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { EditorGroupColumn, columnToEditorGroup } from 'vs/workbench/services/editor/common/editorGroupColumn'; -import { EditorGroupLayout, GroupDirection, GroupLocation, GroupsOrder, IEditorGroup, IEditorGroupsService, isEditorGroup, preferredSideBySideGroupDirection } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { EditorGroupLayout, GroupDirection, GroupLocation, GroupsOrder, IEditorGroup, IEditorGroupsService, IEditorReplacement, isEditorGroup, preferredSideBySideGroupDirection } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorResolverService } from 'vs/workbench/services/editor/common/editorResolverService'; import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; @@ -656,36 +656,42 @@ function registerFocusEditorGroupAtIndexCommands(): void { } } -export function splitEditor(editorGroupService: IEditorGroupsService, direction: GroupDirection, context?: IEditorCommandsContext): void { - let sourceGroup: IEditorGroup | undefined; - if (context && typeof context.groupId === 'number') { - sourceGroup = editorGroupService.getGroup(context.groupId); - } else { - sourceGroup = editorGroupService.activeGroup; - } +export function splitEditor(editorGroupService: IEditorGroupsService, direction: GroupDirection, contexts?: (IEditorCommandsContext | undefined)[]): void { + let newGroup: IEditorGroup | undefined; - if (!sourceGroup) { - return; - } + for (const context of contexts ?? [undefined]) { + let sourceGroup: IEditorGroup | undefined; + if (context && typeof context.groupId === 'number') { + sourceGroup = editorGroupService.getGroup(context.groupId); + } else { + sourceGroup = editorGroupService.activeGroup; + } - // Add group - const newGroup = editorGroupService.addGroup(sourceGroup, direction); + if (!sourceGroup) { + return; + } - // Split editor (if it can be split) - let editorToCopy: EditorInput | undefined; - if (context && typeof context.editorIndex === 'number') { - editorToCopy = sourceGroup.getEditorByIndex(context.editorIndex); - } else { - editorToCopy = sourceGroup.activeEditor ?? undefined; - } + // Add group + if (!newGroup) { + newGroup = editorGroupService.addGroup(sourceGroup, direction); + } - // Copy the editor to the new group, else create an empty group - if (editorToCopy && !editorToCopy.hasCapability(EditorInputCapabilities.Singleton)) { - sourceGroup.copyEditor(editorToCopy, newGroup, { preserveFocus: context?.preserveFocus }); + // Split editor (if it can be split) + let editorToCopy: EditorInput | undefined; + if (context && typeof context.editorIndex === 'number') { + editorToCopy = sourceGroup.getEditorByIndex(context.editorIndex); + } else { + editorToCopy = sourceGroup.activeEditor ?? undefined; + } + + // Copy the editor to the new group, else create an empty group + if (editorToCopy && !editorToCopy.hasCapability(EditorInputCapabilities.Singleton)) { + sourceGroup.copyEditor(editorToCopy, newGroup, { preserveFocus: context?.preserveFocus }); + } } // Focus - newGroup.focus(); + newGroup?.focus(); } function registerSplitEditorCommands() { @@ -696,7 +702,8 @@ function registerSplitEditorCommands() { { id: SPLIT_EDITOR_RIGHT, direction: GroupDirection.RIGHT } ].forEach(({ id, direction }) => { CommandsRegistry.registerCommand(id, function (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) { - splitEditor(accessor.get(IEditorGroupsService), direction, getCommandsContext(resourceOrContext, context)); + const { editors } = getEditorsContext(accessor, resourceOrContext, context); + splitEditor(accessor.get(IEditorGroupsService), direction, editors); }); }); } @@ -875,63 +882,75 @@ function registerCloseEditorCommands() { when: undefined, primary: undefined, handler: async (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { - const editorGroupService = accessor.get(IEditorGroupsService); const editorService = accessor.get(IEditorService); const editorResolverService = accessor.get(IEditorResolverService); const telemetryService = accessor.get(ITelemetryService); - const { group, editor } = resolveCommandsContext(editorGroupService, getCommandsContext(resourceOrContext, context)); + const editorsAndGroup = getEditorsFromContext(accessor, resourceOrContext, context); + const editorReplacements = new Map(); - if (!editor) { - return; - } - const untypedEditor = editor.toUntyped(); + for (const { editor, group } of editorsAndGroup) { + const untypedEditor = editor.toUntyped(); - // Resolver can only resolve untyped editors - if (!untypedEditor) { - return; - } - untypedEditor.options = { ...editorService.activeEditorPane?.options, override: EditorResolution.PICK }; - const resolvedEditor = await editorResolverService.resolveEditor(untypedEditor, group); - if (!isEditorInputWithOptionsAndGroup(resolvedEditor)) { - return; - } + // Resolver can only resolve untyped editors + if (!untypedEditor) { + return; + } + untypedEditor.options = { ...editorService.activeEditorPane?.options, override: EditorResolution.PICK }; + const resolvedEditor = await editorResolverService.resolveEditor(untypedEditor, group); + if (!isEditorInputWithOptionsAndGroup(resolvedEditor)) { + return; + } - // Replace editor with resolved one - await resolvedEditor.group.replaceEditors([ - { + if (!editorReplacements.has(group)) { + editorReplacements.set(group, []); + } + + editorReplacements.get(group)?.push({ editor: editor, replacement: resolvedEditor.editor, forceReplaceDirty: editor.resource?.scheme === Schemas.untitled, options: resolvedEditor.options - } - ]); + }); - type WorkbenchEditorReopenClassification = { - owner: 'rebornix'; - comment: 'Identify how a document is reopened'; - scheme: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'File system provider scheme for the resource' }; - ext: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'File extension for the resource' }; - from: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The editor view type the resource is switched from' }; - to: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The editor view type the resource is switched to' }; - }; + // Telemetry - type WorkbenchEditorReopenEvent = { - scheme: string; - ext: string; - from: string; - to: string; - }; + type WorkbenchEditorReopenClassification = { + owner: 'rebornix'; + comment: 'Identify how a document is reopened'; + scheme: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'File system provider scheme for the resource' }; + ext: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'File extension for the resource' }; + from: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The editor view type the resource is switched from' }; + to: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The editor view type the resource is switched to' }; + }; - telemetryService.publicLog2('workbenchEditorReopen', { - scheme: editor.resource?.scheme ?? '', - ext: editor.resource ? extname(editor.resource) : '', - from: editor.editorId ?? '', - to: resolvedEditor.editor.editorId ?? '' - }); + type WorkbenchEditorReopenEvent = { + scheme: string; + ext: string; + from: string; + to: string; + }; + + telemetryService.publicLog2('workbenchEditorReopen', { + scheme: editor.resource?.scheme ?? '', + ext: editor.resource ? extname(editor.resource) : '', + from: editor.editorId ?? '', + to: resolvedEditor.editor.editorId ?? '' + }); + } + + // Replace editor with resolved one + let group: IEditorGroup | undefined, replacements: IEditorReplacement[] | undefined; + for ([group, replacements] of editorReplacements) { + await group.replaceEditors(replacements); + } + + if (!group || !replacements) { + return; + } // Make sure it becomes active too - await resolvedEditor.group.openEditor(resolvedEditor.editor); + await group?.openEditor(replacements[0].replacement); } }); @@ -1273,11 +1292,8 @@ function registerOtherEditorCommands(): void { when: ActiveEditorStickyContext.toNegated(), primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.Shift | KeyCode.Enter), handler: async (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { - const editorGroupService = accessor.get(IEditorGroupsService); - - const { group, editor } = resolveCommandsContext(editorGroupService, getCommandsContext(resourceOrContext, context)); - if (group && editor) { - return group.stickEditor(editor); + for (const { editor, group } of getEditorsFromContext(accessor, resourceOrContext, context)) { + group.stickEditor(editor); } } }); @@ -1315,11 +1331,8 @@ function registerOtherEditorCommands(): void { when: ActiveEditorStickyContext, primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.Shift | KeyCode.Enter), handler: async (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { - const editorGroupService = accessor.get(IEditorGroupsService); - - const { group, editor } = resolveCommandsContext(editorGroupService, getCommandsContext(resourceOrContext, context)); - if (group && editor) { - return group.unstickEditor(editor); + for (const { editor, group } of getEditorsFromContext(accessor, resourceOrContext, context)) { + group.unstickEditor(editor); } } }); @@ -1367,6 +1380,24 @@ function getEditorsContext(accessor: ServicesAccessor, resourceOrContext?: URI | }; } +export function getEditorsFromContext(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): { editor: EditorInput; group: IEditorGroup }[] { + const { editors, groups } = getEditorsContext(accessor, resourceOrContext, context); + + const editorsAndGroup = editors.map(e => { + if (e.editorIndex === undefined) { + return undefined; + } + const group = groups.find(group => group && group.id === e.groupId); + const editor = group?.getEditorByIndex(e.editorIndex); + if (!editor || !group) { + return undefined; + } + return { editor: editor, group: group }; + }); + + return editorsAndGroup.filter(group => !!group); +} + export function getCommandsContext(resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): IEditorCommandsContext | undefined { if (URI.isUri(resourceOrContext)) { return context; @@ -1432,6 +1463,25 @@ export function getMultiSelectedEditorContexts(editorContext: IEditorCommandsCon return [focus]; } } + // Check editors selected in the group (tabs) + else { + const group = editorContext ? editorGroupService.getGroup(editorContext.groupId) : editorGroupService.activeGroup; + if (group) { + const selectedEditors: EditorInput[] = []; + // If context provides an editor index, use it + if (editorContext && editorContext.editorIndex !== undefined) { + const editor = group?.getEditorByIndex(editorContext.editorIndex); + if (editor && group.isSelected(editor)) { + selectedEditors.push(...group.selectedEditors); + } + } else { + selectedEditors.push(...group.selectedEditors); + } + if (selectedEditors.length > 1) { + return selectedEditors.map(se => ({ groupId: group.id, editorIndex: group.getIndexOfEditor(se) })); + } + } + } // Otherwise go with passed in context return !!editorContext ? [editorContext] : []; diff --git a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts index 14f3ad5b728..aaa77f5babb 100644 --- a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts +++ b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts @@ -307,11 +307,12 @@ class DropOverlay extends Themable { else if (this.editorTransfer.hasData(DraggedEditorIdentifier.prototype)) { const data = this.editorTransfer.getData(DraggedEditorIdentifier.prototype); if (Array.isArray(data)) { - const draggedEditor = data[0].identifier; + const draggedEditors = data; + const firstDraggedEditor = data[0].identifier; - const sourceGroup = this.editorGroupService.getGroup(draggedEditor.groupId); + const sourceGroup = this.editorGroupService.getGroup(firstDraggedEditor.groupId); if (sourceGroup) { - const copyEditor = this.isCopyOperation(event, draggedEditor); + const copyEditor = this.isCopyOperation(event, firstDraggedEditor); let targetGroup: IEditorGroup | undefined = undefined; // Optimization: if we move the last editor of an editor group @@ -328,16 +329,20 @@ class DropOverlay extends Themable { return; } - // Open in target group - const options = fillActiveEditorViewState(sourceGroup, draggedEditor.editor, { - pinned: true, // always pin dropped editor - sticky: sourceGroup.isSticky(draggedEditor.editor), // preserve sticky state - }); + const editors = draggedEditors.map(draggedEditor => ( + { + editor: draggedEditor.identifier.editor, + options: fillActiveEditorViewState(sourceGroup, draggedEditor.identifier.editor, { + pinned: true, // always pin dropped editor + sticky: sourceGroup.isSticky(firstDraggedEditor.editor), // preserve sticky state + }) + } + )); if (!copyEditor) { - sourceGroup.moveEditor(draggedEditor.editor, targetGroup, options); + sourceGroup.moveEditors(editors, targetGroup); } else { - sourceGroup.copyEditor(draggedEditor.editor, targetGroup, options); + sourceGroup.copyEditors(editors, targetGroup); } } diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index dbe3d63a86e..e95a07c45ff 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/editorgroupview'; import { EditorGroupModel, IEditorOpenOptions, IGroupModelChangeEvent, ISerializedEditorGroupModel, isGroupEditorCloseEvent, isGroupEditorOpenEvent, isSerializedEditorGroupModel } from 'vs/workbench/common/editor/editorGroupModel'; import { GroupIdentifier, CloseDirection, IEditorCloseEvent, IEditorPane, SaveReason, IEditorPartOptionsChangeEvent, EditorsOrder, IVisibleEditorPane, EditorResourceAccessor, EditorInputCapabilities, IUntypedEditorInput, DEFAULT_EDITOR_ASSOCIATION, SideBySideEditor, EditorCloseContext, IEditorWillMoveEvent, IEditorWillOpenEvent, IMatchEditorOptions, GroupModelChangeKind, IActiveEditorChangeEvent, IFindEditorOptions, IToolbarActions } from 'vs/workbench/common/editor'; -import { ActiveEditorGroupLockedContext, ActiveEditorDirtyContext, EditorGroupEditorsCountContext, ActiveEditorStickyContext, ActiveEditorPinnedContext, ActiveEditorLastInGroupContext, ActiveEditorFirstInGroupContext, ResourceContextKey, applyAvailableEditorIds, ActiveEditorAvailableEditorIdsContext, ActiveEditorCanSplitInGroupContext, SideBySideEditorActiveContext } from 'vs/workbench/common/contextkeys'; +import { ActiveEditorGroupLockedContext, ActiveEditorDirtyContext, EditorGroupEditorsCountContext, ActiveEditorStickyContext, ActiveEditorPinnedContext, ActiveEditorLastInGroupContext, ActiveEditorFirstInGroupContext, ResourceContextKey, applyAvailableEditorIds, ActiveEditorAvailableEditorIdsContext, ActiveEditorCanSplitInGroupContext, SideBySideEditorActiveContext, MultipleEditorsSelectedContext, TwoEditorsSelectedContext } from 'vs/workbench/common/contextkeys'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { Emitter, Relay } from 'vs/base/common/event'; @@ -252,6 +252,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView { const groupActiveEditorStickyContext = ActiveEditorStickyContext.bindTo(this.scopedContextKeyService); const groupEditorsCountContext = EditorGroupEditorsCountContext.bindTo(this.scopedContextKeyService); const groupLockedContext = ActiveEditorGroupLockedContext.bindTo(this.scopedContextKeyService); + const multipleEditorsSelectedContext = MultipleEditorsSelectedContext.bindTo(this.contextKeyService); + const twoEditorsSelectedContext = TwoEditorsSelectedContext.bindTo(this.contextKeyService); const groupActiveEditorAvailableEditorIds = ActiveEditorAvailableEditorIdsContext.bindTo(this.scopedContextKeyService); const groupActiveEditorCanSplitInGroupContext = ActiveEditorCanSplitInGroupContext.bindTo(this.scopedContextKeyService); @@ -311,6 +313,10 @@ export class EditorGroupView extends Themable implements IEditorGroupView { groupActiveEditorStickyContext.set(this.model.isSticky(this.model.activeEditor)); } break; + case GroupModelChangeKind.EDITOR_SELECTION: + multipleEditorsSelectedContext.set(this.model.selectedEditors.length > 1); + twoEditorsSelectedContext.set(this.model.selectedEditors.length === 2); + break; } // Group editors count context @@ -588,6 +594,9 @@ export class EditorGroupView extends Themable implements IEditorGroupView { case GroupModelChangeKind.EDITOR_LABEL: this.onDidChangeEditorLabel(e.editor); break; + case GroupModelChangeKind.EDITOR_SELECTION: + this.onDidChangeEditorSelection(e.editor); + break; } } @@ -818,6 +827,12 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.titleControl.updateEditorLabel(editor); } + private onDidChangeEditorSelection(editor: EditorInput): void { + + // Forward to title control + this.titleControl.setEditorSelections([editor], this.model.isSelected(editor)); + } + private onDidVisibilityChange(visible: boolean): void { // Forward to active editor pane @@ -936,6 +951,10 @@ export class EditorGroupView extends Themable implements IEditorGroupView { return this.model.activeEditor; } + get selectedEditors(): EditorInput[] { + return this.model.selectedEditors; + } + get previewEditor(): EditorInput | null { return this.model.previewEditor; } @@ -948,6 +967,10 @@ export class EditorGroupView extends Themable implements IEditorGroupView { return this.model.isSticky(editorOrIndex); } + isSelected(editor: EditorInput): boolean { + return this.model.isSelected(editor); + } + isTransient(editorOrIndex: EditorInput | number): boolean { return this.model.isTransient(editorOrIndex); } @@ -956,6 +979,58 @@ export class EditorGroupView extends Themable implements IEditorGroupView { return this.model.isActive(editor); } + async selectEditor(editor: EditorInput, active?: boolean): Promise { + if (active) { + await this.doOpenEditor(editor, { activation: EditorActivation.ACTIVATE }, { selected: true }); + } else { + this.model.selectEditor(editor, active); + } + } + + async selectEditors(editors: EditorInput[], activeEditor?: EditorInput): Promise { + for (const editor of editors) { + await this.selectEditor(editor, editor === activeEditor); + } + } + + async unSelectEditor(editor: EditorInput): Promise { + await this.unSelectEditors([editor]); + } + + async unSelectEditors(editors: EditorInput[]): Promise { + // Check if the active editor is unselected + const unselectingActiveEditor = !!editors.find(editor => this.model.isActive(editor)); + if (unselectingActiveEditor) { + editors = editors.filter(editor => !this.model.isActive(editor)); + } + + // Unselect all none active editors + for (const editor of editors) { + this.model.unselectEditor(editor); + } + + // if the active editor is unselected, make another selected editor active + if (unselectingActiveEditor) { + // do not allow to unselect the active editor if it is the last selected editor + if (this.selectedEditors.length === 1) { + console.warn('Cannot unselect the last selected editor of a group'); + return; + } + + const activeEditor = this.activeEditor; + // Find the next selected editor to make active based on MRU order + const recentEditors = this.model.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE); + for (let i = 1; i < recentEditors.length; i++) { // First one is the active editor + const recentEditor = recentEditors[i]; + if (this.isSelected(recentEditor)) { + await this.doOpenEditor(recentEditor, { activation: EditorActivation.ACTIVATE }, { selected: true }); + this.model.unselectEditor(activeEditor!); + break; + } + } + } + } + contains(candidate: EditorInput | IUntypedEditorInput, options?: IMatchEditorOptions): boolean { return this.model.contains(candidate, options); } @@ -1106,6 +1181,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { pinned, sticky: options?.sticky || (typeof options?.index === 'number' && this.model.isSticky(options.index)), transient: !!options?.transient, + selected: internalOptions?.selected, active: this.count === 0 || !options || !options.inactive, supportSideBySide: internalOptions?.supportSideBySide }; diff --git a/src/vs/workbench/browser/parts/editor/editorTabsControl.ts b/src/vs/workbench/browser/parts/editor/editorTabsControl.ts index d7a82ed5c16..5cbcad63de0 100644 --- a/src/vs/workbench/browser/parts/editor/editorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/editorTabsControl.ts @@ -86,6 +86,7 @@ export interface IEditorTabsControl extends IDisposable { stickEditor(editor: EditorInput): void; unstickEditor(editor: EditorInput): void; setActive(isActive: boolean): void; + setEditorSelections(editor: EditorInput[], selected: boolean): void; updateEditorLabel(editor: EditorInput): void; updateEditorDirty(editor: EditorInput): void; layout(dimensions: IEditorTitleControlDimensions): Dimension; @@ -502,6 +503,8 @@ export abstract class EditorTabsControl extends Themable implements IEditorTabsC abstract setActive(isActive: boolean): void; + abstract setEditorSelections(editor: EditorInput[], selected: boolean): void; + abstract updateEditorLabel(editor: EditorInput): void; abstract updateEditorDirty(editor: EditorInput): void; diff --git a/src/vs/workbench/browser/parts/editor/editorTitleControl.ts b/src/vs/workbench/browser/parts/editor/editorTitleControl.ts index 5ff3995ffbd..e24e15ed611 100644 --- a/src/vs/workbench/browser/parts/editor/editorTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/editorTitleControl.ts @@ -163,6 +163,10 @@ export class EditorTitleControl extends Themable { return this.editorTabsControl.setActive(isActive); } + setEditorSelections(editors: EditorInput[], selected: boolean): void { + this.editorTabsControl.setEditorSelections(editors, selected); + } + updateEditorLabel(editor: EditorInput): void { return this.editorTabsControl.updateEditorLabel(editor); } diff --git a/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css b/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css index 557a5ff4b0f..93559402aa5 100644 --- a/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css @@ -134,6 +134,11 @@ color: var(--vscode-tab-activeForeground); } +.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.selected:not(.active) { + background-color: var(--vscode-tab-selectedBackground); + color: var(--vscode-tab-selectedForeground); +} + .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:not(.active) { box-shadow: none; } @@ -269,6 +274,7 @@ } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active.tab-border-top > .tab-border-top-container, +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.selected.tab-border-top > .tab-border-top-container, .monaco-workbench .part.editor > .content .editor-group-container > .title:not(.two-tab-bars) .tabs-container > .tab.active.tab-border-bottom > .tab-border-bottom-container, .monaco-workbench .part.editor > .content .editor-group-container > .title.two-tab-bars .tabs-and-actions-container:not(:first-child) .tabs-container > .tab.active.tab-border-bottom > .tab-border-bottom-container, .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty-border-top > .tab-border-top-container { @@ -279,7 +285,8 @@ width: 100%; } -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active.tab-border-top > .tab-border-top-container { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active.tab-border-top > .tab-border-top-container, +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.selected.tab-border-top > .tab-border-top-container { z-index: 6; top: 0; height: 1px; diff --git a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts index 706924c23e7..e5d654c5f01 100644 --- a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts @@ -26,13 +26,13 @@ import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElemen import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import { getOrSet } from 'vs/base/common/map'; import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { TAB_INACTIVE_BACKGROUND, TAB_ACTIVE_BACKGROUND, TAB_BORDER, EDITOR_DRAG_AND_DROP_BACKGROUND, TAB_UNFOCUSED_ACTIVE_BACKGROUND, TAB_UNFOCUSED_ACTIVE_BORDER, TAB_ACTIVE_BORDER, TAB_HOVER_BACKGROUND, TAB_HOVER_BORDER, TAB_UNFOCUSED_HOVER_BACKGROUND, TAB_UNFOCUSED_HOVER_BORDER, EDITOR_GROUP_HEADER_TABS_BACKGROUND, WORKBENCH_BACKGROUND, TAB_ACTIVE_BORDER_TOP, TAB_UNFOCUSED_ACTIVE_BORDER_TOP, TAB_ACTIVE_MODIFIED_BORDER, TAB_INACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_ACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_INACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_INACTIVE_BACKGROUND, TAB_HOVER_FOREGROUND, TAB_UNFOCUSED_HOVER_FOREGROUND, EDITOR_GROUP_HEADER_TABS_BORDER, TAB_LAST_PINNED_BORDER } from 'vs/workbench/common/theme'; +import { TAB_INACTIVE_BACKGROUND, TAB_ACTIVE_BACKGROUND, TAB_BORDER, EDITOR_DRAG_AND_DROP_BACKGROUND, TAB_UNFOCUSED_ACTIVE_BACKGROUND, TAB_UNFOCUSED_ACTIVE_BORDER, TAB_ACTIVE_BORDER, TAB_HOVER_BACKGROUND, TAB_HOVER_BORDER, TAB_UNFOCUSED_HOVER_BACKGROUND, TAB_UNFOCUSED_HOVER_BORDER, EDITOR_GROUP_HEADER_TABS_BACKGROUND, WORKBENCH_BACKGROUND, TAB_ACTIVE_BORDER_TOP, TAB_UNFOCUSED_ACTIVE_BORDER_TOP, TAB_ACTIVE_MODIFIED_BORDER, TAB_INACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_ACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_INACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_INACTIVE_BACKGROUND, TAB_HOVER_FOREGROUND, TAB_UNFOCUSED_HOVER_FOREGROUND, EDITOR_GROUP_HEADER_TABS_BORDER, TAB_LAST_PINNED_BORDER, TAB_SELECTED_BORDER_TOP } from 'vs/workbench/common/theme'; import { activeContrastBorder, contrastBorder, editorBackground } from 'vs/platform/theme/common/colorRegistry'; import { ResourcesDropHandler, DraggedEditorIdentifier, DraggedEditorGroupIdentifier, extractTreeDropData, isWindowDraggedOver } from 'vs/workbench/browser/dnd'; import { Color } from 'vs/base/common/color'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { MergeGroupMode, IMergeGroupOptions } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { addDisposableListener, EventType, EventHelper, Dimension, scheduleAtNextAnimationFrame, findParentWithClass, clearNode, DragAndDropObserver, isMouseEvent, getWindow } from 'vs/base/browser/dom'; +import { addDisposableListener, EventType, EventHelper, Dimension, scheduleAtNextAnimationFrame, findParentWithClass, clearNode, DragAndDropObserver, isMouseEvent, getWindow, $, getActiveDocument } from 'vs/base/browser/dom'; import { localize } from 'vs/nls'; import { IEditorGroupsView, EditorServiceImpl, IEditorGroupView, IInternalEditorOpenOptions, IEditorPartsView } from 'vs/workbench/browser/parts/editor/editor'; import { CloseOneEditorAction, UnpinEditorAction } from 'vs/workbench/browser/parts/editor/editorActions'; @@ -56,6 +56,7 @@ import { IEditorTitleControlDimensions } from 'vs/workbench/browser/parts/editor import { StickyEditorGroupModel, UnstickyEditorGroupModel } from 'vs/workbench/common/editor/filteredEditorGroupModel'; import { IReadonlyEditorGroupModel } from 'vs/workbench/common/editor/editorGroupModel'; import { IHostService } from 'vs/workbench/services/host/browser/host'; +import { BugIndicatingError } from 'vs/base/common/errors'; interface IEditorInputLabel { readonly editor: EditorInput; @@ -674,7 +675,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { // Activity has an impact on each tab's active indication this.forEachTab((editor, tabIndex, tabContainer, tabLabelWidget, tabLabel, tabActionBar) => { - this.redrawTabActiveAndDirty(isGroupActive, editor, tabContainer, tabActionBar); + this.redrawTabSelectedActiveAndDirty(isGroupActive, editor, tabContainer, tabActionBar); }); // Activity has an impact on the toolbar, so we need to update and layout @@ -682,6 +683,12 @@ export class MultiEditorTabsControl extends EditorTabsControl { this.layout(this.dimensions, { forceRevealActiveTab: true }); } + setEditorSelections(editors: EditorInput[], selected: boolean): void { + this.forEachTab((editor, tabIndex, tabContainer, tabLabelWidget, tabLabel, tabActionBar) => { + this.redrawTabSelectedActiveAndDirty(this.groupsView.activeGroup === this.groupView, editor, tabContainer, tabActionBar); + }); + } + private updateEditorLabelScheduler = this._register(new RunOnceScheduler(() => this.doUpdateEditorLabels(), 0)); updateEditorLabel(editor: EditorInput): void { @@ -709,7 +716,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { } updateEditorDirty(editor: EditorInput): void { - this.withTab(editor, (editor, tabIndex, tabContainer, tabLabelWidget, tabLabel, tabActionBar) => this.redrawTabActiveAndDirty(this.groupsView.activeGroup === this.groupView, editor, tabContainer, tabActionBar)); + this.withTab(editor, (editor, tabIndex, tabContainer, tabLabelWidget, tabLabel, tabActionBar) => this.redrawTabSelectedActiveAndDirty(this.groupsView.activeGroup === this.groupView, editor, tabContainer, tabActionBar)); } override updateOptions(oldOptions: IEditorPartOptions, newOptions: IEditorPartOptions): void { @@ -853,6 +860,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { return this.groupView.getIndexOfEditor(editor); } + private lastSelectedEditor: EditorInput | undefined; private registerTabListeners(tab: HTMLElement, tabIndex: number, tabsContainer: HTMLElement, tabsScrollbar: ScrollableElement): IDisposable { const disposables = new DisposableStore(); @@ -874,8 +882,28 @@ export class MultiEditorTabsControl extends EditorTabsControl { // Open tabs editor const editor = this.tabsModel.getEditorByIndex(tabIndex); if (editor) { - // Even if focus is preserved make sure to activate the group. - this.groupView.openEditor(editor, { preserveFocus, activation: EditorActivation.ACTIVATE }); + if (e.shiftKey) { + let anchor; + if (this.lastSelectedEditor && this.groupView.isSelected(this.lastSelectedEditor)) { + // The last selected editor is the anchor + anchor = this.lastSelectedEditor; + } else { + // The active editor is the anchor + this.lastSelectedEditor = this.groupView.activeEditor!; + anchor = this.groupView.activeEditor!; + } + this.selectEditorsBetween(editor, anchor); + } else if (e.ctrlKey) { + if (this.groupView.isSelected(editor)) { + this.groupView.unSelectEditor(editor); + } else { + this.groupView.selectEditor(editor, true); + this.lastSelectedEditor = editor; + } + } else { + // Even if focus is preserved make sure to activate the group. + this.groupView.openEditor(editor, { preserveFocus, activation: EditorActivation.ACTIVATE }, { selected: this.groupView.isSelected(editor) /* Ensures drag and drop does not remove selection */ }); + } } return undefined; @@ -904,6 +932,22 @@ export class MultiEditorTabsControl extends EditorTabsControl { EventHelper.stop(e); tab.blur(); + + if (isMouseEvent(e) && (e.button !== 0 /* middle/right mouse button */ || (isMacintosh && e.ctrlKey /* macOS context menu */))) { + if (e.button === 1) { + e.preventDefault(); // required to prevent auto-scrolling (https://github.com/microsoft/vscode/issues/16690) + } + + return undefined; + } + + if (this.originatesFromTabActionBar(e)) { + return; // not when clicking on actions + } + + if (!e.ctrlKey && !e.shiftKey && this.groupView.selectedEditors.length > 1) { + this.unselectAllEditors(); + } })); // Close on mouse middle click @@ -1030,11 +1074,25 @@ export class MultiEditorTabsControl extends EditorTabsControl { isNewWindowOperation = this.isNewWindowOperation(e); - this.editorTransfer.setData([new DraggedEditorIdentifier({ editor, groupId: this.groupView.id })], DraggedEditorIdentifier.prototype); + const draggedEditors = []; + const selectedEditors = this.groupView.selectedEditors; + const isMultiSelected = this.groupView.isSelected(editor) && selectedEditors.length > 1; + if (isMultiSelected) { + draggedEditors.push(...selectedEditors); + } else { + draggedEditors.push(editor); + } + + this.editorTransfer.setData(draggedEditors.map(e => new DraggedEditorIdentifier({ editor: e, groupId: this.groupView.id })), DraggedEditorIdentifier.prototype); if (e.dataTransfer) { e.dataTransfer.effectAllowed = 'copyMove'; - e.dataTransfer.setDragImage(tab, 0, 0); // top left corner of dragged tab set to cursor position to make room for drop-border feedback + if (isMultiSelected) { + const label = `${editor.getName()} + ${draggedEditors.length - 1}`; + setupMultiselectDragLabel(label, e.dataTransfer, tab); + } else { + e.dataTransfer.setDragImage(tab, 0, 0); // top left corner of dragged tab set to cursor position to make room for drop-border feedback + } } // Apply some datatransfer types to allow for dragging the element outside of the application @@ -1082,14 +1140,14 @@ export class MultiEditorTabsControl extends EditorTabsControl { onDragEnd: async e => { this.updateDropFeedback(tab, false, e, tabIndex); - + const draggedEditors = this.editorTransfer.getData(DraggedEditorIdentifier.prototype); this.editorTransfer.clearData(DraggedEditorIdentifier.prototype); - const editor = this.tabsModel.getEditorByIndex(tabIndex); if ( !isNewWindowOperation || isWindowDraggedOver() || - !editor + !draggedEditors || + draggedEditors.length === 0 ) { return; // drag to open in new window is disabled } @@ -1100,10 +1158,11 @@ export class MultiEditorTabsControl extends EditorTabsControl { } const targetGroup = auxiliaryEditorPart.activeGroup; - if (this.isMoveOperation(lastDragEvent ?? e, targetGroup.id, editor)) { - this.groupView.moveEditor(editor, targetGroup); + const editors = draggedEditors.map(de => ({ editor: de.identifier.editor, options: {} })); + if (this.isMoveOperation(lastDragEvent ?? e, targetGroup.id, draggedEditors[0].identifier.editor)) { + this.groupView.moveEditors(editors, targetGroup); } else { - this.groupView.copyEditor(editor, targetGroup); + this.groupView.copyEditors(editors, targetGroup); } targetGroup.focus(); @@ -1118,22 +1177,6 @@ export class MultiEditorTabsControl extends EditorTabsControl { targetIndex++; } - // If we are moving an editor inside the same group and it is - // located before the target index we need to reduce the index - // by one to account for the fact that the move will cause all - // subsequent tabs to move one to the left. - const editorIdentifiers = this.editorTransfer.getData(DraggedEditorIdentifier.prototype); - if (editorIdentifiers !== undefined) { - const draggedEditorIdentifier = editorIdentifiers[0].identifier; - const sourceGroup = this.editorPartsView.getGroup(draggedEditorIdentifier.groupId); - if (sourceGroup?.id === this.groupView.id) { - const editorIndex = sourceGroup.getIndexOfEditor(draggedEditorIdentifier.editor); - if (editorIndex < targetIndex) { - targetIndex--; - } - } - } - this.onDrop(e, targetIndex, tabsContainer); } })); @@ -1234,6 +1277,42 @@ export class MultiEditorTabsControl extends EditorTabsControl { return { leftElement: tabBefore as HTMLElement, rightElement: tabAfter as HTMLElement }; } + private selectEditorsBetween(target: EditorInput, anchor: EditorInput): void { + const editorIndex = this.groupView.getIndexOfEditor(target); + if (editorIndex === -1) { + throw new BugIndicatingError(); + } + + const anchorIndex = this.groupView.getIndexOfEditor(anchor); + if (anchorIndex === -1) { + throw new BugIndicatingError(); + } + + // Unselect editors on other side of anchor in relation to the target + let currentIndex = anchorIndex; + while (currentIndex >= 0 && currentIndex <= this.groupView.count - 1) { + currentIndex = anchorIndex < editorIndex ? currentIndex - 1 : currentIndex + 1; + + const currentEditor = this.groupView.getEditorByIndex(currentIndex); + if (!currentEditor || !this.groupView.isSelected(currentEditor)) { + break; + } + + this.groupView.unSelectEditor(currentEditor); + } + + // Select editors between anchor and target + const fromIndex = anchorIndex < editorIndex ? anchorIndex : editorIndex; + const toIndex = anchorIndex < editorIndex ? editorIndex : anchorIndex; + + const selectedEditors = this.groupView.getEditors(EditorsOrder.SEQUENTIAL).slice(fromIndex, toIndex + 1); + this.groupView.selectEditors(selectedEditors, target); + } + + private unselectAllEditors(): void { + this.groupView.unSelectEditors(this.groupView.selectedEditors.filter(editor => !this.groupView.isActive(editor))); + } + private computeTabLabels(): void { const { labelFormat } = this.groupsView.partOptions; const { verbosity, shortenDuplicates } = this.getLabelConfigFlags(labelFormat); @@ -1452,7 +1531,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { this.redrawTabBorders(tabIndex, tabContainer); // Active / dirty state - this.redrawTabActiveAndDirty(this.groupsView.activeGroup === this.groupView, editor, tabContainer, tabActionBar); + this.redrawTabSelectedActiveAndDirty(this.groupsView.activeGroup === this.groupView, editor, tabContainer, tabActionBar); } private redrawTabLabel(editor: EditorInput, tabIndex: number, tabContainer: HTMLElement, tabLabelWidget: IResourceLabel, tabLabel: IEditorInputLabel): void { @@ -1510,7 +1589,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { } } - private redrawTabActiveAndDirty(isGroupActive: boolean, editor: EditorInput, tabContainer: HTMLElement, tabActionBar: ActionBar): void { + private redrawTabSelectedActiveAndDirty(isGroupActive: boolean, editor: EditorInput, tabContainer: HTMLElement, tabActionBar: ActionBar): void { const isTabActive = this.tabsModel.isActive(editor); const hasModifiedBorderTop = this.doRedrawTabDirty(isGroupActive, isTabActive, editor, tabContainer); @@ -1520,23 +1599,35 @@ export class MultiEditorTabsControl extends EditorTabsControl { private doRedrawTabActive(isGroupActive: boolean, allowBorderTop: boolean, editor: EditorInput, tabContainer: HTMLElement, tabActionBar: ActionBar): void { const isActive = this.tabsModel.isActive(editor); + const isSelected = this.groupView.isSelected(editor); // TODO, move to model tabContainer.classList.toggle('active', isActive); + tabContainer.classList.toggle('selected', isSelected); tabContainer.setAttribute('aria-selected', isActive ? 'true' : 'false'); tabContainer.tabIndex = isActive ? 0 : -1; // Only active tab can be focused into tabActionBar.setFocusable(isActive); + // Set border BOTTOM if theme defined color if (isActive) { - // Set border BOTTOM if theme defined color const activeTabBorderColorBottom = this.getColor(isGroupActive ? TAB_ACTIVE_BORDER : TAB_UNFOCUSED_ACTIVE_BORDER); tabContainer.classList.toggle('tab-border-bottom', !!activeTabBorderColorBottom); tabContainer.style.setProperty('--tab-border-bottom-color', activeTabBorderColorBottom?.toString() ?? ''); - - // Set border TOP if theme defined color - const activeTabBorderColorTop = allowBorderTop ? this.getColor(isGroupActive ? TAB_ACTIVE_BORDER_TOP : TAB_UNFOCUSED_ACTIVE_BORDER_TOP) : undefined; - tabContainer.classList.toggle('tab-border-top', !!activeTabBorderColorTop); - tabContainer.style.setProperty('--tab-border-top-color', activeTabBorderColorTop?.toString() ?? ''); } + + // Set border TOP if theme defined color + let color: string | null = null; + if (allowBorderTop) { + if (isActive) { + color = this.getColor(isGroupActive ? TAB_ACTIVE_BORDER_TOP : TAB_UNFOCUSED_ACTIVE_BORDER_TOP); + } + + if (color === null && isSelected) { + color = this.getColor(TAB_SELECTED_BORDER_TOP); + } + } + + tabContainer.classList.toggle('tab-border-top', !!color); + tabContainer.style.setProperty('--tab-border-top-color', color ?? ''); } private doRedrawTabDirty(isGroupActive: boolean, isTabActive: boolean, editor: EditorInput, tabContainer: HTMLElement): boolean { @@ -2053,7 +2144,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { this.updateDropFeedback(tabsContainer, false, e, targetTabIndex); tabsContainer.classList.remove('scroll'); - const targetEditorIndex = this.tabsModel instanceof UnstickyEditorGroupModel ? targetTabIndex + this.groupView.stickyCount : targetTabIndex; + let targetEditorIndex = this.tabsModel instanceof UnstickyEditorGroupModel ? targetTabIndex + this.groupView.stickyCount : targetTabIndex; const options: IEditorOptions = { sticky: this.tabsModel instanceof StickyEditorGroupModel && this.tabsModel.stickyCount === targetEditorIndex, index: targetEditorIndex @@ -2082,24 +2173,32 @@ export class MultiEditorTabsControl extends EditorTabsControl { else if (this.editorTransfer.hasData(DraggedEditorIdentifier.prototype)) { const data = this.editorTransfer.getData(DraggedEditorIdentifier.prototype); if (Array.isArray(data)) { - const draggedEditor = data[0].identifier; - const sourceGroup = this.editorPartsView.getGroup(draggedEditor.groupId); - if (sourceGroup) { - // Move editor to target position and index - if (this.isMoveOperation(e, draggedEditor.groupId, draggedEditor.editor)) { - sourceGroup.moveEditor(draggedEditor.editor, this.groupView, options); + // Keep the same order when moving / copying editors within the same group + for (const de of data) { + const editor = de.identifier.editor; + const sourceGroup = this.editorPartsView.getGroup(de.identifier.groupId); + if (!sourceGroup) { + continue; } - // Copy editor to target position and index - else { - sourceGroup.copyEditor(draggedEditor.editor, this.groupView, options); + const sourceEditorIndex = sourceGroup.getIndexOfEditor(editor); + if (sourceGroup === this.groupView && sourceEditorIndex < targetEditorIndex) { + targetEditorIndex--; } + + if (this.isMoveOperation(e, de.identifier.groupId, editor)) { + sourceGroup.moveEditor(editor, this.groupView, { ...options, index: targetEditorIndex }); + } else { + sourceGroup.copyEditor(editor, this.groupView, { ...options, index: targetEditorIndex }); + } + + targetEditorIndex++; } - - this.groupView.focus(); - this.editorTransfer.clearData(DraggedEditorIdentifier.prototype); } + + this.groupView.focus(); + this.editorTransfer.clearData(DraggedEditorIdentifier.prototype); } // Check for tree items @@ -2135,6 +2234,22 @@ export class MultiEditorTabsControl extends EditorTabsControl { } } +function setupMultiselectDragLabel(text: string, dataTransfer: DataTransfer, tab: HTMLElement) { + const dragImage = $('.monaco-drag-image'); + dragImage.textContent = text; + const getDragImageContainer = (e: HTMLElement | null) => { + while (e && !e.classList.contains('monaco-workbench')) { + e = e.parentElement; + } + return e || getActiveDocument().body; + }; + + const container = getDragImageContainer(tab); + container.appendChild(dragImage); + dataTransfer.setDragImage(dragImage, -10, -10); + setTimeout(() => container.removeChild(dragImage), 0); +} + registerThemingParticipant((theme, collector) => { // Add bottom border to tabs when wrapping @@ -2195,7 +2310,7 @@ registerThemingParticipant((theme, collector) => { const tabHoverBackground = theme.getColor(TAB_HOVER_BACKGROUND); if (tabHoverBackground) { collector.addRule(` - .monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab:hover { + .monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab:not(.selected):hover { background-color: ${tabHoverBackground} !important; } `); @@ -2204,7 +2319,7 @@ registerThemingParticipant((theme, collector) => { const tabUnfocusedHoverBackground = theme.getColor(TAB_UNFOCUSED_HOVER_BACKGROUND); if (tabUnfocusedHoverBackground) { collector.addRule(` - .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:hover { + .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:not(.selected):hover { background-color: ${tabUnfocusedHoverBackground} !important; } `); @@ -2214,7 +2329,7 @@ registerThemingParticipant((theme, collector) => { const tabHoverForeground = theme.getColor(TAB_HOVER_FOREGROUND); if (tabHoverForeground) { collector.addRule(` - .monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab:hover { + .monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab:not(.selected):hover { color: ${tabHoverForeground} !important; } `); @@ -2223,7 +2338,7 @@ registerThemingParticipant((theme, collector) => { const tabUnfocusedHoverForeground = theme.getColor(TAB_UNFOCUSED_HOVER_FOREGROUND); if (tabUnfocusedHoverForeground) { collector.addRule(` - .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:hover { + .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:not(.selected):hover { color: ${tabUnfocusedHoverForeground} !important; } `); diff --git a/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts index 24d85415eb8..50683f16fe0 100644 --- a/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts @@ -163,6 +163,14 @@ export class MultiRowEditorControl extends Disposable implements IEditorTabsCont this.unstickyEditorTabsControl.setActive(isActive); } + setEditorSelections(editors: EditorInput[], selected: boolean): void { + const stickyEditors = editors.filter(e => this.model.isSticky(e)); + const unstickyEditors = editors.filter(e => !this.model.isSticky(e)); + + this.stickyEditorTabsControl.setEditorSelections(stickyEditors, selected); + this.unstickyEditorTabsControl.setEditorSelections(unstickyEditors, selected); + } + updateEditorLabel(editor: EditorInput): void { this.getEditorTabsController(editor).updateEditorLabel(editor); } diff --git a/src/vs/workbench/browser/parts/editor/noEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/noEditorTabsControl.ts index baf08bb4504..68eda55c923 100644 --- a/src/vs/workbench/browser/parts/editor/noEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/noEditorTabsControl.ts @@ -61,6 +61,8 @@ export class NoEditorTabsControl extends EditorTabsControl { setActive(isActive: boolean): void { } + setEditorSelections(editor: EditorInput[], selected: boolean): void { } + updateEditorLabel(editor: EditorInput): void { } updateEditorDirty(editor: EditorInput): void { } diff --git a/src/vs/workbench/browser/parts/editor/singleEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/singleEditorTabsControl.ts index c33305205c2..8e69d2ab994 100644 --- a/src/vs/workbench/browser/parts/editor/singleEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/singleEditorTabsControl.ts @@ -191,6 +191,8 @@ export class SingleEditorTabsControl extends EditorTabsControl { this.redraw(); } + setEditorSelections(editor: EditorInput[], selected: boolean): void { } + updateEditorLabel(editor: EditorInput): void { this.ifEditorIsActive(editor, () => this.redraw()); } diff --git a/src/vs/workbench/common/contextkeys.ts b/src/vs/workbench/common/contextkeys.ts index 1c82f8542c8..63780b3830e 100644 --- a/src/vs/workbench/common/contextkeys.ts +++ b/src/vs/workbench/common/contextkeys.ts @@ -72,6 +72,8 @@ export const ActiveEditorGroupLastContext = new RawContextKey('activeEd export const ActiveEditorGroupLockedContext = new RawContextKey('activeEditorGroupLocked', false, localize('activeEditorGroupLocked', "Whether the active editor group is locked")); export const MultipleEditorGroupsContext = new RawContextKey('multipleEditorGroups', false, localize('multipleEditorGroups', "Whether there are multiple editor groups opened")); export const SingleEditorGroupsContext = MultipleEditorGroupsContext.toNegated(); +export const MultipleEditorsSelectedContext = new RawContextKey('multipleEditorsSelected', false, localize('multipleEditorsSelected', "Whether multiple editors have been selected")); +export const TwoEditorsSelectedContext = new RawContextKey('twoEditorsSelected', false, localize('twoEditorsSelected', "Whether two editors have been selected")); // Editor Part Context Keys export const EditorPartMultipleEditorGroupsContext = new RawContextKey('editorPartMultipleEditorGroups', false, localize('editorPartMultipleEditorGroups', "Whether there are multiple editor groups opened in an editor part")); diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index bd2c42d8510..c1e16b6929b 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -1171,6 +1171,7 @@ export const enum GroupModelChangeKind { EDITOR_CAPABILITIES, EDITOR_PIN, EDITOR_TRANSIENT, + EDITOR_SELECTION, EDITOR_STICKY, EDITOR_DIRTY, EDITOR_WILL_DISPOSE diff --git a/src/vs/workbench/common/editor/editorGroupModel.ts b/src/vs/workbench/common/editor/editorGroupModel.ts index 30c3ef5e9ca..ab75d18ab3f 100644 --- a/src/vs/workbench/common/editor/editorGroupModel.ts +++ b/src/vs/workbench/common/editor/editorGroupModel.ts @@ -25,6 +25,7 @@ export interface IEditorOpenOptions { readonly sticky?: boolean; readonly transient?: boolean; active?: boolean; + readonly selected?: boolean; readonly index?: number; readonly supportSideBySide?: SideBySideEditor.ANY | SideBySideEditor.BOTH; } @@ -174,6 +175,7 @@ export interface IReadonlyEditorGroupModel { readonly isLocked: boolean; readonly activeEditor: EditorInput | null; readonly previewEditor: EditorInput | null; + readonly selectedEditors: EditorInput[]; getEditors(order: EditorsOrder, options?: { excludeSticky?: boolean }): EditorInput[]; getEditorByIndex(index: number): EditorInput | undefined; @@ -181,6 +183,7 @@ export interface IReadonlyEditorGroupModel { isActive(editor: EditorInput | IUntypedEditorInput): boolean; isPinned(editorOrIndex: EditorInput | number): boolean; isSticky(editorOrIndex: EditorInput | number): boolean; + isSelected(editor: EditorInput | number): boolean; isTransient(editorOrIndex: EditorInput | number): boolean; isFirst(editor: EditorInput, editors?: EditorInput[]): boolean; isLast(editor: EditorInput, editors?: EditorInput[]): boolean; @@ -193,6 +196,8 @@ interface IEditorGroupModel extends IReadonlyEditorGroupModel { closeEditor(editor: EditorInput, context?: EditorCloseContext, openNext?: boolean): IEditorCloseResult | undefined; moveEditor(editor: EditorInput, toIndex: number): EditorInput | undefined; setActive(editor: EditorInput | undefined): EditorInput | undefined; + selectEditor(editor: EditorInput, active?: boolean): EditorInput | undefined; + unselectEditor(editor: EditorInput): EditorInput | undefined; } export class EditorGroupModel extends Disposable implements IEditorGroupModel { @@ -216,10 +221,17 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { private locked = false; - private preview: EditorInput | null = null; // editor in preview state - private active: EditorInput | null = null; // editor in active state - private sticky = -1; // index of first editor in sticky state - private readonly transient = new Set(); // editors in transient state + private selected: EditorInput[] = []; // editors in selected state, first one is active + private set active(editor: EditorInput | null) { + this.selected = editor ? [editor] : []; + } + private get active(): EditorInput | null { + return this.selected[0] || null; + } + + private preview: EditorInput | null = null; // editor in preview state + private sticky = -1; // index of first editor in sticky state + private readonly transient = new Set(); // editors in transient state private editorOpenPositioning: ('left' | 'right' | 'first' | 'last') | undefined; private focusRecentEditorAfterClose: boolean | undefined; @@ -403,7 +415,7 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { // Handle active if (makeActive) { - this.doSetActive(newEditor, targetIndex); + this.doSetActive(newEditor, targetIndex, options?.selected); } return { @@ -426,7 +438,7 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { // Activate it if (makeActive) { - this.doSetActive(existingEditor, existingEditorIndex); + this.doSetActive(existingEditor, existingEditorIndex, options?.selected); } // Respect index @@ -546,7 +558,8 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { const sticky = this.isSticky(index); // Active Editor closed - if (openNext && this.matches(this.active, editor)) { + const isActiveEditor = this.matches(this.active, editor); + if (openNext && isActiveEditor) { // More than one editor if (this.mru.length > 1) { @@ -569,6 +582,13 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { this.active = null; } } + // Remove from selection + else if (!isActiveEditor) { + const wasSelected = !!this.selected.find(selected => this.matches(selected, editor)); + if (wasSelected) { + this.doSetSelected(editor, index, false); + } + } // Preview Editor closed if (this.matches(this.preview, editor)) { @@ -671,12 +691,17 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { return editor; } - private doSetActive(editor: EditorInput, editorIndex: number): void { + private doSetActive(editor: EditorInput, editorIndex: number, selected: boolean = false): void { if (this.matches(this.active, editor)) { return; // already active } - this.active = editor; + if (selected) { + this.selected = this.selected.filter(selected => selected !== editor); + this.selected.unshift(editor); + } else { + this.selected = [editor]; + } // Bring to front in MRU list const mruIndex = this.indexOf(editor, this.mru); @@ -690,6 +715,85 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { editorIndex }; this._onDidModelChange.fire(event); + + const selectionEvent: IGroupEditorChangeEvent = { + kind: GroupModelChangeKind.EDITOR_SELECTION, + editor, + editorIndex + }; + this._onDidModelChange.fire(selectionEvent); + } + + public get selectedEditors(): EditorInput[] { + // Return selected editors in sequential order + return this.editors.filter(editor => this.isSelected(editor)); + } + + isSelected(editor: number | EditorInput): boolean { + if (typeof editor === 'number') { + editor = this.editors[editor]; + } + + return !!this.selected.find(selectedEditor => this.matches(selectedEditor, editor)); + } + + selectEditor(candidate: EditorInput, active: boolean = false): EditorInput | undefined { + const res = this.findEditor(candidate); + if (!res) { + return; // not found + } + + const [editor, editorIndex] = res; + + this.doSetSelected(editor, editorIndex, true, active); + + return editor; + } + + unselectEditor(candidate: EditorInput): EditorInput | undefined { + const res = this.findEditor(candidate); + if (!res) { + return; // not found + } + + const [editor, editorIndex] = res; + + this.doSetSelected(editor, editorIndex, false); + + return editor; + } + + private doSetSelected(editor: EditorInput, editorIndex: number, select: boolean, active: boolean = false): void { + if (select) { + if (this.isSelected(editor)) { + return; + } + + if (active) { + this.doSetActive(editor, editorIndex, true); + } else { + this.selected.push(editor); + } + } else { + if (!this.isSelected(editor)) { + return; + } + + if (this.matches(this.active, editor)) { + console.warn('Cannot unselect the active editor'); + return; + } + + this.selected.splice(this.selected.indexOf(editor), 1); + } + + // Event + const event: IGroupEditorChangeEvent = { + kind: GroupModelChangeKind.EDITOR_SELECTION, + editor, + editorIndex + }; + this._onDidModelChange.fire(event); } setIndex(index: number) { diff --git a/src/vs/workbench/common/editor/filteredEditorGroupModel.ts b/src/vs/workbench/common/editor/filteredEditorGroupModel.ts index 390b19874c8..787ee09f001 100644 --- a/src/vs/workbench/common/editor/filteredEditorGroupModel.ts +++ b/src/vs/workbench/common/editor/filteredEditorGroupModel.ts @@ -36,11 +36,13 @@ abstract class FilteredEditorGroupModel extends Disposable implements IReadonlyE get activeEditor(): EditorInput | null { return this.model.activeEditor && this.filter(this.model.activeEditor) ? this.model.activeEditor : null; } get previewEditor(): EditorInput | null { return this.model.previewEditor && this.filter(this.model.previewEditor) ? this.model.previewEditor : null; } + get selectedEditors(): EditorInput[] { return this.model.selectedEditors.filter(e => this.filter(e)); } isPinned(editorOrIndex: EditorInput | number): boolean { return this.model.isPinned(editorOrIndex); } isTransient(editorOrIndex: EditorInput | number): boolean { return this.model.isTransient(editorOrIndex); } isSticky(editorOrIndex: EditorInput | number): boolean { return this.model.isSticky(editorOrIndex); } isActive(editor: EditorInput | IUntypedEditorInput): boolean { return this.model.isActive(editor); } + isSelected(editor: number | EditorInput): boolean { return this.model.isSelected(editor); } isFirst(editor: EditorInput): boolean { return this.model.isFirst(editor, this.getEditors(EditorsOrder.SEQUENTIAL)); diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index b6a90af3469..3412f81c3bf 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -166,6 +166,28 @@ export const TAB_UNFOCUSED_ACTIVE_BORDER_TOP = registerColor('tab.unfocusedActiv hcLight: '#B5200D' }, localize('tabActiveUnfocusedBorderTop', "Border to the top of an active tab in an unfocused group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); +export const TAB_SELECTED_BORDER_TOP = registerColor('tab.selectedBorderTop', { + dark: TAB_ACTIVE_BORDER_TOP, + light: TAB_ACTIVE_BORDER_TOP, + hcDark: TAB_ACTIVE_BORDER_TOP, + hcLight: TAB_ACTIVE_BORDER_TOP +}, localize('tabSelectedBorderTop', "Border to the top of a selected tab. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); + +export const TAB_SELECTED_BACKGROUND = registerColor('tab.selectedBackground', { + dark: TAB_ACTIVE_BACKGROUND, + light: TAB_ACTIVE_BACKGROUND, + hcDark: TAB_ACTIVE_BACKGROUND, + hcLight: TAB_ACTIVE_BACKGROUND +}, localize('tabSelectedBackground', "Background of a selected tab. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); + +export const TAB_SELECTED_FOREGROUND = registerColor('tab.selectedForeground', { + dark: TAB_ACTIVE_FOREGROUND, + light: TAB_ACTIVE_FOREGROUND, + hcDark: TAB_ACTIVE_FOREGROUND, + hcLight: TAB_ACTIVE_FOREGROUND +}, localize('tabSelectedForeground', "Foreground of a selected tab. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); + + export const TAB_HOVER_BORDER = registerColor('tab.hoverBorder', { dark: null, light: null, diff --git a/src/vs/workbench/contrib/externalTerminal/browser/externalTerminal.contribution.ts b/src/vs/workbench/contrib/externalTerminal/browser/externalTerminal.contribution.ts index 2e6373fe684..6ca5f7d48f0 100644 --- a/src/vs/workbench/contrib/externalTerminal/browser/externalTerminal.contribution.ts +++ b/src/vs/workbench/contrib/externalTerminal/browser/externalTerminal.contribution.ts @@ -26,6 +26,7 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle import { Registry } from 'vs/platform/registry/common/platform'; import { IExternalTerminalConfiguration, IExternalTerminalService } from 'vs/platform/externalTerminal/common/externalTerminal'; import { TerminalLocation } from 'vs/platform/terminal/common/terminal'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; const OPEN_IN_TERMINAL_COMMAND_ID = 'openInTerminal'; const OPEN_IN_INTEGRATED_TERMINAL_COMMAND_ID = 'openInIntegratedTerminal'; @@ -47,7 +48,7 @@ function registerOpenTerminalCommand(id: string, explorerKind: 'integrated' | 'e } catch { } - const resources = getMultiSelectedResources(resource, accessor.get(IListService), editorService, accessor.get(IExplorerService)); + const resources = getMultiSelectedResources(resource, accessor.get(IListService), editorService, accessor.get(IEditorGroupsService), accessor.get(IExplorerService)); return fileService.resolveAll(resources.map(r => ({ resource: r }))).then(async stats => { // Always use integrated terminal when using a remote const config = configurationService.getValue(); diff --git a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts index 88b9f76da0c..663156554b5 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts @@ -20,7 +20,7 @@ import { CLOSE_SAVED_EDITORS_COMMAND_ID, CLOSE_EDITORS_IN_GROUP_COMMAND_ID, CLOS import { AutoSaveAfterShortDelayContext } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { WorkbenchListDoubleSelection } from 'vs/platform/list/browser/listService'; import { Schemas } from 'vs/base/common/network'; -import { DirtyWorkingCopiesContext, EnterMultiRootWorkspaceSupportContext, HasWebFileSystemAccess, WorkbenchStateContext, WorkspaceFolderCountContext, SidebarFocusContext, ActiveEditorCanRevertContext, ActiveEditorContext, ResourceContextKey, ActiveEditorAvailableEditorIdsContext } from 'vs/workbench/common/contextkeys'; +import { DirtyWorkingCopiesContext, EnterMultiRootWorkspaceSupportContext, HasWebFileSystemAccess, WorkbenchStateContext, WorkspaceFolderCountContext, SidebarFocusContext, ActiveEditorCanRevertContext, ActiveEditorContext, ResourceContextKey, ActiveEditorAvailableEditorIdsContext, MultipleEditorsSelectedContext, TwoEditorsSelectedContext } from 'vs/workbench/common/contextkeys'; import { IsWebContext } from 'vs/platform/contextkey/common/contextkeys'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ThemeIcon } from 'vs/base/common/themables'; @@ -155,18 +155,19 @@ const copyRelativePathCommand = { }; // Editor Title Context Menu -appendEditorTitleContextMenuItem(COPY_PATH_COMMAND_ID, copyPathCommand.title, ResourceContextKey.IsFileSystemResource, '1_cutcopypaste'); -appendEditorTitleContextMenuItem(COPY_RELATIVE_PATH_COMMAND_ID, copyRelativePathCommand.title, ResourceContextKey.IsFileSystemResource, '1_cutcopypaste'); -appendEditorTitleContextMenuItem(REVEAL_IN_EXPLORER_COMMAND_ID, nls.localize('revealInSideBar', "Reveal in Explorer View"), ResourceContextKey.IsFileSystemResource, '2_files', 1); +appendEditorTitleContextMenuItem(COPY_PATH_COMMAND_ID, copyPathCommand.title, ResourceContextKey.IsFileSystemResource, '1_cutcopypaste', true); +appendEditorTitleContextMenuItem(COPY_RELATIVE_PATH_COMMAND_ID, copyRelativePathCommand.title, ResourceContextKey.IsFileSystemResource, '1_cutcopypaste', true); +appendEditorTitleContextMenuItem(REVEAL_IN_EXPLORER_COMMAND_ID, nls.localize('revealInSideBar', "Reveal in Explorer View"), ResourceContextKey.IsFileSystemResource, '2_files', false, 1); -export function appendEditorTitleContextMenuItem(id: string, title: string, when: ContextKeyExpression | undefined, group: string, order?: number): void { +export function appendEditorTitleContextMenuItem(id: string, title: string, when: ContextKeyExpression | undefined, group: string, supportsMultiSelect: boolean, order?: number): void { + const precondition = supportsMultiSelect !== true ? MultipleEditorsSelectedContext.negate() : undefined; // Menu MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { - command: { id, title }, + command: { id, title, precondition }, when, group, - order + order, }); } @@ -415,6 +416,13 @@ MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { when: ContextKeyExpr.and(ResourceContextKey.HasResource, WorkbenchListDoubleSelection, isFileOrUntitledResourceContextKey) }); +MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { + group: '3_compare', + order: 30, + command: compareSelectedCommand, + when: ContextKeyExpr.and(ResourceContextKey.HasResource, TwoEditorsSelectedContext, isFileOrUntitledResourceContextKey) +}); + MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { group: '4_close', order: 10, diff --git a/src/vs/workbench/contrib/files/browser/fileCommands.ts b/src/vs/workbench/contrib/files/browser/fileCommands.ts index da1e9c4dcd5..81bf68c3e0b 100644 --- a/src/vs/workbench/contrib/files/browser/fileCommands.ts +++ b/src/vs/workbench/contrib/files/browser/fileCommands.ts @@ -90,10 +90,11 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ }, id: OPEN_TO_SIDE_COMMAND_ID, handler: async (accessor, resource: URI | object) => { const editorService = accessor.get(IEditorService); + const editorGroupService = accessor.get(IEditorGroupsService); const listService = accessor.get(IListService); const fileService = accessor.get(IFileService); const explorerService = accessor.get(IExplorerService); - const resources = getMultiSelectedResources(resource, listService, editorService, explorerService); + const resources = getMultiSelectedResources(resource, listService, editorService, editorGroupService, explorerService); // Set side input if (resources.length) { @@ -203,8 +204,9 @@ CommandsRegistry.registerCommand({ id: COMPARE_SELECTED_COMMAND_ID, handler: async (accessor, resource: URI | object) => { const editorService = accessor.get(IEditorService); + const editorGroupService = accessor.get(IEditorGroupsService); const explorerService = accessor.get(IExplorerService); - const resources = getMultiSelectedResources(resource, accessor.get(IListService), editorService, explorerService); + const resources = getMultiSelectedResources(resource, accessor.get(IListService), editorService, editorGroupService, explorerService); if (resources.length === 2) { return editorService.openEditor({ @@ -253,7 +255,7 @@ async function resourcesToClipboard(resources: URI[], relative: boolean, clipboa } const copyPathCommandHandler: ICommandHandler = async (accessor, resource: URI | object) => { - const resources = getMultiSelectedResources(resource, accessor.get(IListService), accessor.get(IEditorService), accessor.get(IExplorerService)); + const resources = getMultiSelectedResources(resource, accessor.get(IListService), accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IExplorerService)); await resourcesToClipboard(resources, false, accessor.get(IClipboardService), accessor.get(ILabelService), accessor.get(IConfigurationService)); }; @@ -280,7 +282,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ }); const copyRelativePathCommandHandler: ICommandHandler = async (accessor, resource: URI | object) => { - const resources = getMultiSelectedResources(resource, accessor.get(IListService), accessor.get(IEditorService), accessor.get(IExplorerService)); + const resources = getMultiSelectedResources(resource, accessor.get(IListService), accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IExplorerService)); await resourcesToClipboard(resources, true, accessor.get(IClipboardService), accessor.get(ILabelService), accessor.get(IConfigurationService)); }; @@ -571,7 +573,7 @@ CommandsRegistry.registerCommand({ const contextService = accessor.get(IWorkspaceContextService); const uriIdentityService = accessor.get(IUriIdentityService); const workspace = contextService.getWorkspace(); - const resources = getMultiSelectedResources(resource, accessor.get(IListService), accessor.get(IEditorService), accessor.get(IExplorerService)).filter(resource => + const resources = getMultiSelectedResources(resource, accessor.get(IListService), accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IExplorerService)).filter(resource => workspace.folders.some(folder => uriIdentityService.extUri.isEqual(folder.uri, resource)) // Need to verify resources are workspaces since multi selection can trigger this command on some non workspace resources ); diff --git a/src/vs/workbench/contrib/files/browser/files.ts b/src/vs/workbench/contrib/files/browser/files.ts index b3e8260dde4..7fc74195991 100644 --- a/src/vs/workbench/contrib/files/browser/files.ts +++ b/src/vs/workbench/contrib/files/browser/files.ts @@ -105,7 +105,7 @@ export function getResourceForCommand(resource: URI | object | undefined, listSe return EditorResourceAccessor.getOriginalUri(editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY }); } -export function getMultiSelectedResources(resource: URI | object | undefined, listService: IListService, editorService: IEditorService, explorerService: IExplorerService): Array { +export function getMultiSelectedResources(resource: URI | object | undefined, listService: IListService, editorService: IEditorService, editorGroupService: IEditorGroupsService, explorerService: IExplorerService): Array { const list = listService.lastFocusedList; const element = list?.getHTMLElement(); if (element && isActiveElement(element)) { @@ -137,6 +137,15 @@ export function getMultiSelectedResources(resource: URI | object | undefined, li } } + const activeGroup = editorGroupService.activeGroup; + const selection = activeGroup.selectedEditors; + if (selection.length) { + const selectedResources = selection.map(editor => EditorResourceAccessor.getOriginalUri(editor)).filter(uri => !!uri) as URI[]; + if (selectedResources.some(r => r.toString() === resource?.toString())) { + return selectedResources; + } + } + const result = getResourceForCommand(resource, listService, editorService); return !!result ? [result] : []; } diff --git a/src/vs/workbench/contrib/files/electron-sandbox/fileActions.contribution.ts b/src/vs/workbench/contrib/files/electron-sandbox/fileActions.contribution.ts index de8140c144b..8cb362e422c 100644 --- a/src/vs/workbench/contrib/files/electron-sandbox/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/electron-sandbox/fileActions.contribution.ts @@ -22,6 +22,7 @@ import { ResourceContextKey } from 'vs/workbench/common/contextkeys'; import { appendToCommandPalette, appendEditorTitleContextMenuItem } from 'vs/workbench/contrib/files/browser/fileActions.contribution'; import { SideBySideEditor, EditorResourceAccessor } from 'vs/workbench/common/editor'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; const REVEAL_IN_OS_COMMAND_ID = 'revealFileInOS'; const REVEAL_IN_OS_LABEL = isWindows ? nls.localize2('revealInWindows', "Reveal in File Explorer") : isMacintosh ? nls.localize2('revealInMac', "Reveal in Finder") : nls.localize2('openContainer', "Open Containing Folder"); @@ -36,7 +37,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KeyR }, handler: (accessor: ServicesAccessor, resource: URI | object) => { - const resources = getMultiSelectedResources(resource, accessor.get(IListService), accessor.get(IEditorService), accessor.get(IExplorerService)); + const resources = getMultiSelectedResources(resource, accessor.get(IListService), accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IExplorerService)); revealResourcesInOS(resources, accessor.get(INativeHostService), accessor.get(IWorkspaceContextService)); } }); @@ -57,7 +58,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } }); -appendEditorTitleContextMenuItem(REVEAL_IN_OS_COMMAND_ID, REVEAL_IN_OS_LABEL.value, REVEAL_IN_OS_WHEN_CONTEXT, '2_files', 0); +appendEditorTitleContextMenuItem(REVEAL_IN_OS_COMMAND_ID, REVEAL_IN_OS_LABEL.value, REVEAL_IN_OS_WHEN_CONTEXT, '2_files', false, 0); // Menu registration - open editors diff --git a/src/vs/workbench/contrib/search/browser/searchActionsFind.ts b/src/vs/workbench/contrib/search/browser/searchActionsFind.ts index bd4676d881c..e848ced9bb3 100644 --- a/src/vs/workbench/contrib/search/browser/searchActionsFind.ts +++ b/src/vs/workbench/contrib/search/browser/searchActionsFind.ts @@ -33,6 +33,7 @@ import { category, getElementsToOperateOn, getSearchView, openSearchView } from import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { Schemas } from 'vs/base/common/network'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; //#region Interfaces @@ -317,7 +318,7 @@ async function searchWithFolderCommand(accessor: ServicesAccessor, isFromExplore let resources: URI[]; if (isFromExplorer) { - resources = getMultiSelectedResources(resource, listService, accessor.get(IEditorService), accessor.get(IExplorerService)); + resources = getMultiSelectedResources(resource, listService, accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IExplorerService)); } else { const searchView = getSearchView(accessor.get(IViewsService)); if (!searchView) { diff --git a/src/vs/workbench/services/editor/common/editorGroupsService.ts b/src/vs/workbench/services/editor/common/editorGroupsService.ts index af155c4ccfc..cb9c84b7590 100644 --- a/src/vs/workbench/services/editor/common/editorGroupsService.ts +++ b/src/vs/workbench/services/editor/common/editorGroupsService.ts @@ -649,6 +649,12 @@ export interface IEditorGroup { */ readonly activeEditor: EditorInput | null; + /** + * All selected editor in this group in sequential order. + * The active editor is always part of the selection. + */ + readonly selectedEditors: EditorInput[]; + /** * The editor in the group that is in preview mode if any. There can * only ever be one editor in preview mode. @@ -767,6 +773,34 @@ export interface IEditorGroup { */ isActive(editor: EditorInput | IUntypedEditorInput): boolean; + /** + * Selects the editor in the group. If active is set to true, + * it will be the active editor in the group. + */ + selectEditor(editor: EditorInput, active?: boolean): Promise; + + + /** + * Selects the editors in the group. If activeEditor is provided, + * it will be the active editor in the group. + */ + selectEditors(editors: EditorInput[], activeEditor?: EditorInput): Promise; + + /** + * Unselects the editor in the group. If the editor is not specified, unselects the active editor. + */ + unSelectEditor(editor: EditorInput): Promise; + + /** + * Unselects the editors in the group. If the editor is not specified, unselects the active editor. + */ + unSelectEditors(editors: EditorInput[]): Promise; + + /** + * Whether the editor is selected in the group. + */ + isSelected(editor: EditorInput): boolean; + /** * Find out if a certain editor is included in the group. * diff --git a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts index 5d60cb8afdd..56c29a69a4f 100644 --- a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts @@ -1538,6 +1538,73 @@ suite('EditorGroupsService', () => { assert.strictEqual(group.getIndexOfEditor(inputSticky), 0); }); + test('selection: select/unselect, isSelected/getSelectedEditors', async () => { + const [part] = await createPart(); + const group = part.activeGroup; + + const input1 = createTestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); + const input2 = createTestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); + const input3 = createTestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); + + function isSelection(inputs: TestFileEditorInput[]): boolean { + for (const input of inputs) { + if (group.selectedEditors.indexOf(input) === -1) { + return false; + } + } + return inputs.length === group.selectedEditors.length; + } + + await group.openEditors([input1, input2, input3].map(editor => ({ editor, options: { pinned: true } }))); + + // Active: input1, Selected: input1 + assert.strictEqual(group.isActive(input1), true); + assert.strictEqual(group.isSelected(input1), true); + assert.strictEqual(group.isSelected(input2), false); + assert.strictEqual(group.isSelected(input3), false); + + assert.strictEqual(isSelection([input1]), true); + + await group.selectEditor(input3); + + // Active: input1, Selected: input1, input3 + assert.strictEqual(group.isActive(input1), true); + assert.strictEqual(group.isSelected(input1), true); + assert.strictEqual(group.isSelected(input2), false); + assert.strictEqual(group.isSelected(input3), true); + + assert.strictEqual(isSelection([input1, input3]), true); + + await group.selectEditor(input2, true); + + // Active: input2, Selected: input1, input3 + assert.strictEqual(group.isSelected(input1), true); + assert.strictEqual(group.isActive(input2), true); + assert.strictEqual(group.isSelected(input2), true); + assert.strictEqual(group.isSelected(input3), true); + + assert.strictEqual(isSelection([input1, input2, input3]), true); + + await group.unSelectEditor(input2); + + // Selected: input3 + assert.strictEqual(group.isActive(input1), true); + assert.strictEqual(group.isSelected(input1), true); + assert.strictEqual(group.isSelected(input2), false); + assert.strictEqual(group.isSelected(input3), true); + + assert.strictEqual(isSelection([input1, input3]), true); + + await group.unSelectEditors([input1]); + + // Selected: NONE + assert.strictEqual(group.isSelected(input1), false); + assert.strictEqual(group.isSelected(input2), false); + assert.strictEqual(group.isSelected(input3), true); + + assert.strictEqual(isSelection([input3]), true); + }); + test('moveEditor with context (across groups)', async () => { const [part] = await createPart(); const group = part.activeGroup; diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index e045e101f15..8974dc8ae57 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -884,6 +884,7 @@ export class TestEditorGroupView implements IEditorGroupView { groupsView: IEditorGroupsView = undefined!; activeEditorPane!: IVisibleEditorPane; activeEditor!: EditorInput; + selectedEditors: EditorInput[] = []; previewEditor!: EditorInput; count!: number; stickyCount!: number; @@ -927,6 +928,11 @@ export class TestEditorGroupView implements IEditorGroupView { isSticky(_editor: EditorInput): boolean { return false; } isTransient(_editor: EditorInput): boolean { return false; } isActive(_editor: EditorInput | IUntypedEditorInput): boolean { return false; } + selectEditor(_editor: EditorInput, _active?: boolean): Promise { throw new Error('not implemented'); } + selectEditors(_editors: EditorInput[], _activeEditor?: EditorInput): Promise { throw new Error('not implemented'); } + unSelectEditor(_editor: EditorInput): Promise { throw new Error('not implemented'); } + unSelectEditors(_editors: EditorInput[]): Promise { throw new Error('not implemented'); } + isSelected(_editor: EditorInput): boolean { return false; } contains(candidate: EditorInput | IUntypedEditorInput): boolean { return false; } moveEditor(_editor: EditorInput, _target: IEditorGroup, _options?: IEditorOptions): boolean { return true; } moveEditors(_editors: EditorInputWithOptions[], _target: IEditorGroup): boolean { return true; } From d1b5d420128a8c1a615febcebce21d67c9c7460d Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Mon, 13 May 2024 12:13:40 -0700 Subject: [PATCH 145/357] Fix { languageId ??= ''; - const newText = this.fixCodeText(value, languageId); - this.codeBlockModelCollection.update(this._model.sessionId, model, codeBlockIndex++, { text: newText, languageId }); + this.codeBlockModelCollection.update(this._model.sessionId, model, codeBlockIndex++, { text: value, languageId }); return ''; }; marked.parse(this.ensureFencedCodeBlocksTerminated(content), { renderer }); } - private fixCodeText(text: string, languageId: string): string { - if (languageId === 'php') { - if (!text.trim().startsWith('<')) { - return ``; - } - } - - return text; - } - /** * Marked doesn't consistently render fenced code blocks that aren't terminated. * diff --git a/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts b/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts index f364b9e78ab..a857b64cb1d 100644 --- a/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts +++ b/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts @@ -64,7 +64,7 @@ export class CodeBlockModelCollection extends Disposable { const entry = this.getOrCreate(sessionId, chat, codeBlockIndex); const extractedVulns = extractVulnerabilitiesFromText(content.text); - const newText = extractedVulns.newText; + const newText = fixCodeText(extractedVulns.newText, content.languageId); this.setVulns(sessionId, chat, codeBlockIndex, extractedVulns.vulnerabilities); const textModel = (await entry.model).textEditorModel; @@ -137,3 +137,13 @@ export class CodeBlockModelCollection extends Disposable { }; } } + +function fixCodeText(text: string, languageId: string | undefined): string { + if (languageId === 'php') { + if (!text.trim().startsWith('<')) { + return ` Date: Mon, 13 May 2024 22:31:02 +0200 Subject: [PATCH 146/357] `canSendRequest` should take `LanguageModelChat` object and not an identifier (#212632) --- .../api/common/extHostLanguageModels.ts | 19 ++++++++++++++----- .../vscode.proposed.chatProvider.d.ts | 2 +- .../vscode.proposed.languageModels.d.ts | 3 +-- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/api/common/extHostLanguageModels.ts b/src/vs/workbench/api/common/extHostLanguageModels.ts index e4145c224ca..1953fccb277 100644 --- a/src/vs/workbench/api/common/extHostLanguageModels.ts +++ b/src/vs/workbench/api/common/extHostLanguageModels.ts @@ -465,13 +465,22 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { get onDidChange() { return Event.any(_onDidChangeAccess, _onDidAddRemove); }, - canSendRequest(languageModelId: string): boolean | undefined { + canSendRequest(chat: vscode.LanguageModelChat): boolean | undefined { - const data = that._allLanguageModelData.get(languageModelId); - if (!data) { + let metadata: ILanguageModelChatMetadata | undefined; + + out: for (const [_, value] of that._allLanguageModelData) { + for (const candidate of value.apiObjects.values()) { + if (candidate === chat) { + metadata = value.metadata; + break out; + } + } + } + if (!metadata) { return undefined; } - if (!that._isUsingAuth(from.identifier, data.metadata)) { + if (!that._isUsingAuth(from.identifier, metadata)) { return true; } @@ -479,7 +488,7 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { if (!list) { return undefined; } - return list.has(data.metadata.extension); + return list.has(metadata.extension); } }; } diff --git a/src/vscode-dts/vscode.proposed.chatProvider.d.ts b/src/vscode-dts/vscode.proposed.chatProvider.d.ts index 55f0a2a383b..de344194cfc 100644 --- a/src/vscode-dts/vscode.proposed.chatProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatProvider.d.ts @@ -56,7 +56,7 @@ declare module 'vscode' { export interface ChatResponseProviderMetadata { // limit this provider to some extensions - extensions: string[]; + extensions?: string[]; } export namespace chat { diff --git a/src/vscode-dts/vscode.proposed.languageModels.d.ts b/src/vscode-dts/vscode.proposed.languageModels.d.ts index cae4cb4f135..17115a67305 100644 --- a/src/vscode-dts/vscode.proposed.languageModels.d.ts +++ b/src/vscode-dts/vscode.proposed.languageModels.d.ts @@ -303,9 +303,8 @@ declare module 'vscode' { * model does not exist or consent hasn't been asked for. */ // TODO@API applies to chat and embeddings models - // TODO@API use LanguageModelChat // TODO@API name: canUse, hasAccess? - canSendRequest(languageModelId: string): boolean | undefined; + canSendRequest(chat: LanguageModelChat): boolean | undefined; } export interface ExtensionContext { From 8845f69304a09a901271e928bdce175b40c5cdf0 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Mon, 13 May 2024 13:33:00 -0700 Subject: [PATCH 147/357] don't show terminal chat hint for reconnected terminals (#212636) fix #212278 --- .../chat/browser/terminal.initialHint.contribution.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts index 90c223f507f..544366a3bd6 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts @@ -103,7 +103,7 @@ export class TerminalInitialHintContribution extends Disposable implements ITerm private _createHint(): void { const instance = this._instance instanceof TerminalInstance ? this._instance : undefined; const commandDetectionCapability = instance?.capabilities.get(TerminalCapability.CommandDetection); - if (!instance || !this._xterm || this._hintWidget || !commandDetectionCapability || !commandDetectionCapability?.hasInput) { + if (!instance || !this._xterm || this._hintWidget || !commandDetectionCapability || !commandDetectionCapability?.hasInput || instance.reconnectionProperties) { return; } From 85f4f0326524b97502ca4fa29aa5833e2e43cef5 Mon Sep 17 00:00:00 2001 From: Dat Nguyen Date: Mon, 13 May 2024 16:41:12 -0400 Subject: [PATCH 148/357] Added setting for notebook cell markdown lineheight (#212531) --- .../notebook/browser/notebook.contribution.ts | 6 ++++++ .../notebook/browser/notebookEditorWidget.ts | 1 + .../contrib/notebook/browser/notebookOptions.ts | 13 +++++++++++++ .../browser/view/renderers/backLayerWebView.ts | 3 +++ .../contrib/notebook/common/notebookCommon.ts | 1 + 5 files changed, 24 insertions(+) diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 842b3915640..72eb95a92cf 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -941,6 +941,12 @@ configurationRegistry.registerConfiguration({ default: 0, tags: ['notebookLayout'] }, + [NotebookSetting.markdownLineHeight]: { + markdownDescription: nls.localize('notebook.markdown.lineHeight', "Controls the line height in pixels of markdown cells in notebooks. When set to {0}, {1} will be used", '`0`', '`normal`'), + type: 'number', + default: 0, + tags: ['notebookLayout'] + }, [NotebookSetting.cellEditorOptionsCustomizations]: editorOptionsCustomizationSchema, [NotebookSetting.interactiveWindowCollapseCodeCells]: { markdownDescription: nls.localize('notebook.interactiveWindow.collapseCodeCells', "Controls whether code cells in the interactive window are collapsed by default."), diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 23a4daf925a..05156eebb72 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -366,6 +366,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD || e.dragAndDropEnabled || e.fontSize || e.markupFontSize + || e.markdownLineHeight || e.fontFamily || e.insertToolbarAlignment || e.outputFontSize diff --git a/src/vs/workbench/contrib/notebook/browser/notebookOptions.ts b/src/vs/workbench/contrib/notebook/browser/notebookOptions.ts index 6cc994b7ff6..46ce122a004 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookOptions.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookOptions.ts @@ -47,6 +47,7 @@ export interface NotebookDisplayOptions { // TODO @Yoyokrazy rename to a more ge outputFontFamily: string; outputLineHeight: number; markupFontSize: number; + markdownLineHeight: number; editorOptionsCustomizations: Partial<{ 'editor.indentSize': 'tabSize' | number; 'editor.tabSize': number; @@ -96,6 +97,7 @@ export interface NotebookOptionsChangeEvent { readonly fontSize?: boolean; readonly outputFontSize?: boolean; readonly markupFontSize?: boolean; + readonly markdownLineHeight?: boolean; readonly fontFamily?: boolean; readonly outputFontFamily?: boolean; readonly editorOptionsCustomizations?: boolean; @@ -159,6 +161,7 @@ export class NotebookOptions extends Disposable { // const { bottomToolbarGap, bottomToolbarHeight } = this._computeBottomToolbarDimensions(compactView, insertToolbarPosition, insertToolbarAlignment); const fontSize = this.configurationService.getValue('editor.fontSize'); const markupFontSize = this.configurationService.getValue(NotebookSetting.markupFontSize); + const markdownLineHeight = this.configurationService.getValue(NotebookSetting.markdownLineHeight); let editorOptionsCustomizations = this.configurationService.getValue(NotebookSetting.markupFontSize); } + if (markdownLineHeight) { + configuration.markdownLineHeight = this.configurationService.getValue(NotebookSetting.markdownLineHeight); + } + if (outputFontFamily) { configuration.outputFontFamily = this.configurationService.getValue(NotebookSetting.outputFontFamily); } @@ -579,6 +589,7 @@ export class NotebookOptions extends Disposable { fontSize, outputFontSize, markupFontSize, + markdownLineHeight, fontFamily, outputFontFamily, editorOptionsCustomizations, @@ -796,6 +807,7 @@ export class NotebookOptions extends Disposable { outputFontSize: this._layoutConfiguration.outputFontSize, outputFontFamily: this._layoutConfiguration.outputFontFamily, markupFontSize: this._layoutConfiguration.markupFontSize, + markdownLineHeight: this._layoutConfiguration.markdownLineHeight, outputLineHeight: this._layoutConfiguration.outputLineHeight, outputScrolling: this._layoutConfiguration.outputScrolling, outputWordWrap: this._layoutConfiguration.outputWordWrap, @@ -819,6 +831,7 @@ export class NotebookOptions extends Disposable { outputFontSize: this._layoutConfiguration.outputFontSize, outputFontFamily: this._layoutConfiguration.outputFontFamily, markupFontSize: this._layoutConfiguration.markupFontSize, + markdownLineHeight: this._layoutConfiguration.markdownLineHeight, outputLineHeight: this._layoutConfiguration.outputLineHeight, outputScrolling: this._layoutConfiguration.outputScrolling, outputWordWrap: this._layoutConfiguration.outputWordWrap, diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index cf7965b0752..702182063b3 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -114,6 +114,7 @@ interface BacklayerWebviewOptions { readonly fontFamily: string; readonly outputFontFamily: string; readonly markupFontSize: number; + readonly markdownLineHeight: number; readonly outputLineHeight: number; readonly outputScrolling: boolean; readonly outputWordWrap: boolean; @@ -266,6 +267,7 @@ export class BackLayerWebView extends Themable { 'notebook-output-node-left-padding': `${this.options.outputNodeLeftPadding}px`, 'notebook-markdown-min-height': `${this.options.previewNodePadding * 2}px`, 'notebook-markup-font-size': typeof this.options.markupFontSize === 'number' && this.options.markupFontSize > 0 ? `${this.options.markupFontSize}px` : `calc(${this.options.fontSize}px * 1.2)`, + 'notebook-markdown-line-height': typeof this.options.markdownLineHeight === 'number' && this.options.markdownLineHeight > 0 ? `${this.options.markdownLineHeight}px` : `normal`, 'notebook-cell-output-font-size': `${this.options.outputFontSize || this.options.fontSize}px`, 'notebook-cell-output-line-height': `${this.options.outputLineHeight}px`, 'notebook-cell-output-max-height': `${this.options.outputLineHeight * this.options.outputLineLimit}px`, @@ -366,6 +368,7 @@ export class BackLayerWebView extends Themable { white-space: initial; font-size: var(--notebook-markup-font-size); + line-height: var(--notebook-markdown-line-height); color: var(--theme-ui-foreground); } diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index c269db86ed4..a9c0398c1a7 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -932,6 +932,7 @@ export const NotebookSetting = { openGettingStarted: 'notebook.experimental.openGettingStarted', globalToolbarShowLabel: 'notebook.globalToolbarShowLabel', markupFontSize: 'notebook.markup.fontSize', + markdownLineHeight: 'notebook.markdown.lineHeight', interactiveWindowCollapseCodeCells: 'interactiveWindow.collapseCellInputCode', outputScrollingDeprecated: 'notebook.experimental.outputScrolling', outputScrolling: 'notebook.output.scrolling', From 6643db734a19d8486214204996e122c9756774c6 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Mon, 13 May 2024 14:30:14 -0700 Subject: [PATCH 149/357] Add chat variable id and rename to 'references' (#212480) * Add chat variable id * Rename 'variables' to 'references' * Replace other 'variables' usages * Fix tests --- .../src/singlefolder-tests/chat.test.ts | 4 +-- .../api/browser/mainThreadChatAgents2.ts | 2 +- .../workbench/api/common/extHost.api.impl.ts | 4 +-- .../workbench/api/common/extHost.protocol.ts | 1 + .../api/common/extHostChatVariables.ts | 6 ++-- .../api/common/extHostTypeConverters.ts | 13 +++++---- src/vs/workbench/api/common/extHostTypes.ts | 6 ++-- .../contrib/chat/browser/chatFollowups.ts | 2 +- .../contrib/chat/browser/chatVariables.ts | 4 +-- .../browser/contrib/chatDynamicVariables.ts | 3 ++ .../contrib/chat/common/chatModel.ts | 6 +++- .../contrib/chat/common/chatParserTypes.ts | 11 ++++--- .../contrib/chat/common/chatRequestParser.ts | 6 ++-- .../contrib/chat/common/chatServiceImpl.ts | 3 +- .../contrib/chat/common/chatVariables.ts | 4 +++ .../chat/test/browser/chatVariables.test.ts | 4 +-- ..._agents_and_variables_and_multiline.0.snap | 2 ++ ..._and_variables_and_multiline__part2.0.snap | 2 ++ ...tParser_variable_with_question_mark.0.snap | 1 + .../ChatRequestParser_variables.0.snap | 1 + .../test/common/chatRequestParser.test.ts | 6 ++++ .../browser/inlineChatSessionServiceImpl.ts | 5 ++-- .../vscode.proposed.chatParticipant.d.ts | 29 ++++++++++++------- ...ode.proposed.chatParticipantAdditions.d.ts | 3 +- .../vscode.proposed.chatVariableResolver.d.ts | 3 +- 25 files changed, 89 insertions(+), 42 deletions(-) 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 19de2b62057..6ec0e74eea8 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts @@ -71,7 +71,7 @@ suite('chat', () => { }); test('participant and variable', async () => { - disposables.push(chat.registerChatVariableResolver('myVar', 'My variable', { + disposables.push(chat.registerChatVariableResolver('myVarId', 'myVar', 'My variable', 'My variable', { resolve(_name, _context, _token) { return [{ level: ChatVariableLevel.Full, value: 'myValue' }]; } @@ -81,7 +81,7 @@ suite('chat', () => { commands.executeCommand('workbench.action.chat.open', { query: '@participant hi #myVar' }); const request = await deferred.p; assert.strictEqual(request.prompt, 'hi #myVar'); - assert.strictEqual(request.variables[0].value, 'myValue'); + assert.strictEqual(request.references[0].value, 'myValue'); }); test('result metadata is returned to the followup provider', async () => { diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index ff489e53f35..e5ecb109309 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -274,7 +274,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA kind: CompletionItemKind.Text, detail: v.detail, documentation: v.documentation, - command: { id: AddDynamicVariableAction.ID, title: '', arguments: [{ widget, range: rangeAfterInsert, variableData: revive(v.value) as any, command: v.command } satisfies IAddDynamicVariableContext] } + command: { id: AddDynamicVariableAction.ID, title: '', arguments: [{ id: v.id, widget, range: rangeAfterInsert, variableData: revive(v.value) as any, command: v.command } satisfies IAddDynamicVariableContext] } } satisfies CompletionItem; }); diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 8a34a9e8919..ba39e1a01b4 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1411,9 +1411,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'chatProvider'); return extHostLanguageModels.registerLanguageModel(extension, id, provider, metadata); }, - registerChatVariableResolver(name: string, description: string, resolver: vscode.ChatVariableResolver) { + registerChatVariableResolver(id: string, name: string, userDescription: string, modelDescription: string | undefined, resolver: vscode.ChatVariableResolver) { checkProposedApiEnabled(extension, 'chatVariableResolver'); - return extHostChatVariables.registerVariableResolver(extension, name, description, resolver); + return extHostChatVariables.registerVariableResolver(extension, id, name, userDescription, modelDescription, resolver); }, registerMappedEditsProvider(selector: vscode.DocumentSelector, provider: vscode.MappedEditsProvider) { checkProposedApiEnabled(extension, 'mappedEditsProvider'); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index c4b0014ae38..9bf6786d9cf 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1251,6 +1251,7 @@ export interface MainThreadChatAgentsShape2 extends IDisposable { } export interface IChatAgentCompletionItem { + id: string; insertText?: string; label: string | languages.CompletionItemLabel; value: IChatRequestVariableValueDto; diff --git a/src/vs/workbench/api/common/extHostChatVariables.ts b/src/vs/workbench/api/common/extHostChatVariables.ts index c0587e4ef06..df3ecb47190 100644 --- a/src/vs/workbench/api/common/extHostChatVariables.ts +++ b/src/vs/workbench/api/common/extHostChatVariables.ts @@ -52,10 +52,10 @@ export class ExtHostChatVariables implements ExtHostChatVariablesShape { return undefined; } - registerVariableResolver(extension: IExtensionDescription, name: string, description: string, resolver: vscode.ChatVariableResolver): IDisposable { + registerVariableResolver(extension: IExtensionDescription, id: string, name: string, userDescription: string, modelDescription: string | undefined, resolver: vscode.ChatVariableResolver): IDisposable { const handle = ExtHostChatVariables._idPool++; - this._resolver.set(handle, { extension, data: { name, description }, resolver: resolver }); - this._proxy.$registerVariable(handle, { name, description }); + this._resolver.set(handle, { extension, data: { id, name, description: userDescription, modelDescription }, resolver: resolver }); + this._proxy.$registerVariable(handle, { id, name, description: userDescription, modelDescription }); return toDisposable(() => { this._resolver.delete(handle); diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index b5dcd844713..b6591ad6f9b 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -2545,7 +2545,7 @@ export namespace ChatAgentRequest { command: request.command, attempt: request.attempt ?? 0, enableCommandDetection: request.enableCommandDetection ?? true, - variables: request.variables.variables.map(ChatAgentValueReference.to), + references: request.variables.variables.map(ChatAgentValueReference.to), location: ChatLocation.to(request.location), acceptedConfirmationData: request.acceptedConfirmationData, rejectedConfirmationData: request.rejectedConfirmationData @@ -2565,16 +2565,18 @@ export namespace ChatLocation { } export namespace ChatAgentValueReference { - export function to(request: IChatRequestVariableEntry): vscode.ChatValueReference { - const value = request.value; + export function to(variable: IChatRequestVariableEntry): vscode.ChatValueReference { + const value = variable.value; if (!value) { throw new Error('Invalid value reference'); } return { - name: request.name, - range: (request.range && [request.range.start, request.range.endExclusive])!, // TODO + id: variable.id, + name: variable.name, + range: (variable.range && [variable.range.start, variable.range.endExclusive])!, // TODO value: isUriComponents(value) ? URI.revive(value) : value, + modelDescription: variable.modelDescription }; } } @@ -2582,6 +2584,7 @@ export namespace ChatAgentValueReference { export namespace ChatAgentCompletionItem { export function from(item: vscode.ChatCompletionItem, commandsConverter: CommandsConverter, disposables: DisposableStore): extHostProtocol.IChatAgentCompletionItem { return { + id: item.id, label: item.label, value: item.values[0].value, insertText: item.insertText, diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 4112f9a4a0c..d6f6ea737c7 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -4270,6 +4270,7 @@ export enum ChatVariableLevel { } export class ChatCompletionItem implements vscode.ChatCompletionItem { + id: string; label: string | CompletionItemLabel; insertText?: string; values: vscode.ChatVariableValue[]; @@ -4277,7 +4278,8 @@ export class ChatCompletionItem implements vscode.ChatCompletionItem { documentation?: string | MarkdownString; command?: vscode.Command; - constructor(label: string | CompletionItemLabel, values: vscode.ChatVariableValue[]) { + constructor(id: string, label: string | CompletionItemLabel, values: vscode.ChatVariableValue[]) { + this.id = id; this.label = label; this.values = values; } @@ -4423,7 +4425,7 @@ export class ChatRequestTurn implements vscode.ChatRequestTurn { constructor( readonly prompt: string, readonly command: string | undefined, - readonly variables: vscode.ChatValueReference[], + readonly references: vscode.ChatValueReference[], readonly participant: string, ) { } } diff --git a/src/vs/workbench/contrib/chat/browser/chatFollowups.ts b/src/vs/workbench/contrib/chat/browser/chatFollowups.ts index b4ee434a778..f0f4e3e0afa 100644 --- a/src/vs/workbench/contrib/chat/browser/chatFollowups.ts +++ b/src/vs/workbench/contrib/chat/browser/chatFollowups.ts @@ -63,7 +63,7 @@ export class ChatFollowups extend const tooltip = tooltipPrefix + ('tooltip' in followup && followup.tooltip || baseTitle); - const button = this._register(new Button(container, { ...this.options, supportIcons: true, title: tooltip })); + const button = this._register(new Button(container, { ...this.options, title: tooltip })); if (followup.kind === 'reply') { button.element.classList.add('interactive-followup-reply'); } else if (followup.kind === 'command') { diff --git a/src/vs/workbench/contrib/chat/browser/chatVariables.ts b/src/vs/workbench/contrib/chat/browser/chatVariables.ts index 3051205846d..8474766e0e9 100644 --- a/src/vs/workbench/contrib/chat/browser/chatVariables.ts +++ b/src/vs/workbench/contrib/chat/browser/chatVariables.ts @@ -49,12 +49,12 @@ export class ChatVariablesService implements IChatVariablesService { }; jobs.push(data.resolver(prompt.text, part.variableArg, model, variableProgressCallback, token).then(value => { if (value) { - resolvedVariables[i] = { name: part.variableName, range: part.range, value, references }; + resolvedVariables[i] = { id: data.data.id, modelDescription: data.data.modelDescription, name: part.variableName, range: part.range, value, references }; } }).catch(onUnexpectedExternalError)); } } else if (part instanceof ChatRequestDynamicVariablePart) { - resolvedVariables[i] = { name: part.referenceText, range: part.range, value: part.data }; + resolvedVariables[i] = { id: part.id, name: part.referenceText, range: part.range, value: part.data }; } }); diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts index 12163ceaf03..3f0f72edd27 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts @@ -208,6 +208,7 @@ export class SelectAndInsertFileAction extends Action2 { } context.widget.getContrib(ChatDynamicVariableModel.ID)?.addReference({ + id: 'vscode.file', range: { startLineNumber: range.startLineNumber, startColumn: range.startColumn, endLineNumber: range.endLineNumber, endColumn: range.startColumn + text.length }, data: resource }); @@ -216,6 +217,7 @@ export class SelectAndInsertFileAction extends Action2 { registerAction2(SelectAndInsertFileAction); export interface IAddDynamicVariableContext { + id: string; widget: IChatWidget; range: IRange; variableData: IChatRequestVariableValue; @@ -275,6 +277,7 @@ export class AddDynamicVariableAction extends Action2 { } context.widget.getContrib(ChatDynamicVariableModel.ID)?.addReference({ + id: context.id, range: range, data: variableData }); diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index ed7ea466711..a626cfb8731 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -24,7 +24,9 @@ import { IChatAgentMarkdownContentWithVulnerability, IChatCommandButton, IChatCo import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables'; export interface IChatRequestVariableEntry { + id: string; name: string; + modelDescription?: string; range?: IOffsetRange; value: IChatRequestVariableValue; references?: IChatContentReference[]; @@ -692,12 +694,14 @@ export class ChatModel extends Disposable implements IChatModel { ? raw : { variables: [] }; - variableData.variables = variableData.variables.map(v => { + variableData.variables = variableData.variables.map((v): IChatRequestVariableEntry => { if ('values' in v && Array.isArray(v.values)) { return { + id: v.id ?? '', name: v.name, value: v.values[0]?.value, range: v.range, + modelDescription: v.modelDescription, references: v.references }; } else { diff --git a/src/vs/workbench/contrib/chat/common/chatParserTypes.ts b/src/vs/workbench/contrib/chat/common/chatParserTypes.ts index d5643a26534..6d5cd8c0a39 100644 --- a/src/vs/workbench/contrib/chat/common/chatParserTypes.ts +++ b/src/vs/workbench/contrib/chat/common/chatParserTypes.ts @@ -54,7 +54,7 @@ export const chatSubcommandLeader = '/'; export class ChatRequestVariablePart implements IParsedChatRequestPart { static readonly Kind = 'var'; readonly kind = ChatRequestVariablePart.Kind; - constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly variableName: string, readonly variableArg: string) { } + constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly variableName: string, readonly variableArg: string, readonly variableId: string) { } get text(): string { const argPart = this.variableArg ? `:${this.variableArg}` : ''; @@ -123,7 +123,7 @@ export class ChatRequestSlashCommandPart implements IParsedChatRequestPart { export class ChatRequestDynamicVariablePart implements IParsedChatRequestPart { static readonly Kind = 'dynamic'; readonly kind = ChatRequestDynamicVariablePart.Kind; - constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly text: string, readonly data: IChatRequestVariableValue) { } + constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly text: string, readonly id: string, readonly modelDescription: string | undefined, readonly data: IChatRequestVariableValue) { } get referenceText(): string { return this.text.replace(chatVariableLeader, ''); @@ -149,7 +149,8 @@ export function reviveParsedChatRequest(serialized: IParsedChatRequest): IParsed new OffsetRange(part.range.start, part.range.endExclusive), part.editorRange, (part as ChatRequestVariablePart).variableName, - (part as ChatRequestVariablePart).variableArg + (part as ChatRequestVariablePart).variableArg, + (part as ChatRequestVariablePart).variableName || '', ); } else if (part.kind === ChatRequestAgentPart.Kind) { let agent = (part as ChatRequestAgentPart).agent; @@ -183,7 +184,9 @@ export function reviveParsedChatRequest(serialized: IParsedChatRequest): IParsed new OffsetRange(part.range.start, part.range.endExclusive), part.editorRange, (part as ChatRequestDynamicVariablePart).text, - revive((part as ChatRequestDynamicVariablePart).data) as any + (part as ChatRequestDynamicVariablePart).id, + (part as ChatRequestDynamicVariablePart).modelDescription, + revive((part as ChatRequestDynamicVariablePart).data) ); } else { throw new Error(`Unknown chat request part: ${part.kind}`); diff --git a/src/vs/workbench/contrib/chat/common/chatRequestParser.ts b/src/vs/workbench/contrib/chat/common/chatRequestParser.ts index d0aed8771d0..6df98cbdaaf 100644 --- a/src/vs/workbench/contrib/chat/common/chatRequestParser.ts +++ b/src/vs/workbench/contrib/chat/common/chatRequestParser.ts @@ -149,7 +149,9 @@ export class ChatRequestParser { const varEditorRange = new Range(position.lineNumber, position.column, position.lineNumber, position.column + full.length); if (this.variableService.hasVariable(name)) { - return new ChatRequestVariablePart(varRange, varEditorRange, name, variableArg); + // TODO - not really handling duplicate variables names yet + const id = this.variableService.getVariable(name)!.id ?? ''; + return new ChatRequestVariablePart(varRange, varEditorRange, name, variableArg, id); } return; @@ -209,7 +211,7 @@ export class ChatRequestParser { const length = refAtThisPosition.range.endColumn - refAtThisPosition.range.startColumn; const text = message.substring(0, length); const range = new OffsetRange(offset, offset + length); - return new ChatRequestDynamicVariablePart(range, refAtThisPosition.range, text, refAtThisPosition.data); + return new ChatRequestDynamicVariablePart(range, refAtThisPosition.range, text, refAtThisPosition.id, refAtThisPosition.modelDescription, refAtThisPosition.data); } return; diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index c7ab1fdb48c..821e0b6875d 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -578,8 +578,9 @@ export class ChatService extends Disposable implements IChatService { const implicitVariables = agent.defaultImplicitVariables; if (implicitVariables) { const resolvedImplicitVariables = await Promise.all(implicitVariables.map(async v => { + const id = this.chatVariablesService.getVariable(v)?.id ?? ''; const value = await this.chatVariablesService.resolveVariable(v, parsedRequest.text, model, progressCallback, token); - return value ? { name: v, value } satisfies IChatRequestVariableEntry : + return value ? { id, name: v, value } satisfies IChatRequestVariableEntry : undefined; })); updatedVariableData.variables.push(...coalesce(resolvedImplicitVariables)); diff --git a/src/vs/workbench/contrib/chat/common/chatVariables.ts b/src/vs/workbench/contrib/chat/common/chatVariables.ts index 056620616e0..5cdc87bf082 100644 --- a/src/vs/workbench/contrib/chat/common/chatVariables.ts +++ b/src/vs/workbench/contrib/chat/common/chatVariables.ts @@ -14,8 +14,10 @@ import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserT import { IChatContentReference, IChatProgressMessage } from 'vs/workbench/contrib/chat/common/chatService'; export interface IChatVariableData { + id: string; name: string; description: string; + modelDescription?: string; hidden?: boolean; canTakeArgument?: boolean; } @@ -49,5 +51,7 @@ export interface IChatVariablesService { export interface IDynamicVariable { range: IRange; + id: string; + modelDescription?: string; data: IChatRequestVariableValue; } 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 492a80f54a0..530752d4118 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatVariables.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatVariables.test.ts @@ -41,8 +41,8 @@ suite('ChatVariables', function () { test('ChatVariables - resolveVariables', async function () { - const v1 = service.registerVariable({ name: 'foo', description: 'bar' }, async () => 'farboo'); - const v2 = service.registerVariable({ name: 'far', description: 'boo' }, async () => 'farboo'); + const v1 = service.registerVariable({ id: 'id', name: 'foo', description: 'bar' }, async () => 'farboo'); + const v2 = service.registerVariable({ id: 'id', name: 'far', description: 'boo' }, async () => 'farboo'); const parser = instantiationService.createInstance(ChatRequestParser); 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 776a2546830..0a210c51cfc 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 @@ -90,6 +90,7 @@ }, variableName: "selection", variableArg: "", + variableId: "copilot.selection", kind: "var" }, { @@ -119,6 +120,7 @@ }, variableName: "debugConsole", variableArg: "", + variableId: "copilot.debugConsole", kind: "var" } ], 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 60d1a54726f..874558504ef 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 @@ -59,6 +59,7 @@ }, variableName: "selection", variableArg: "", + variableId: "copilot.selection", kind: "var" }, { @@ -88,6 +89,7 @@ }, variableName: "debugConsole", variableArg: "", + variableId: "copilot.debugConsole", kind: "var" } ], diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_variable_with_question_mark.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_variable_with_question_mark.0.snap index 17b89a36298..d826520078a 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_variable_with_question_mark.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_variable_with_question_mark.0.snap @@ -27,6 +27,7 @@ }, variableName: "selection", variableArg: "", + variableId: "copilot.selection", kind: "var" }, { diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_variables.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_variables.0.snap index 5af3a10d3b8..8334f7ba2b0 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_variables.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_variables.0.snap @@ -27,6 +27,7 @@ }, variableName: "selection", variableArg: "", + variableId: "copilot.selection", kind: "var" }, { 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 6d4713e110d..c0b7a9542a4 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts @@ -89,6 +89,7 @@ suite('ChatRequestParser', () => { test('variables', async () => { varService.hasVariable.returns(true); + varService.getVariable.returns({ id: 'copilot.selection' }); parser = instantiationService.createInstance(ChatRequestParser); const text = 'What does #selection mean?'; @@ -98,6 +99,7 @@ suite('ChatRequestParser', () => { test('variable with question mark', async () => { varService.hasVariable.returns(true); + varService.getVariable.returns({ id: 'copilot.selection' }); parser = instantiationService.createInstance(ChatRequestParser); const text = 'What is #selection?'; @@ -184,6 +186,8 @@ suite('ChatRequestParser', () => { instantiationService.stub(IChatAgentService, agentsService as any); varService.hasVariable.returns(true); + varService.getVariable.onCall(0).returns({ id: 'copilot.selection' }); + varService.getVariable.onCall(1).returns({ id: 'copilot.debugConsole' }); parser = instantiationService.createInstance(ChatRequestParser); const result = parser.parseChatRequest('1', '@agent /subCommand \nPlease do with #selection\nand #debugConsole'); @@ -196,6 +200,8 @@ suite('ChatRequestParser', () => { instantiationService.stub(IChatAgentService, agentsService as any); varService.hasVariable.returns(true); + varService.getVariable.onCall(0).returns({ id: 'copilot.selection' }); + varService.getVariable.onCall(1).returns({ id: 'copilot.debugConsole' }); parser = instantiationService.createInstance(ChatRequestParser); const result = parser.parseChatRequest('1', '@agent Please \ndo /subCommand with #selection\nand #debugConsole'); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts index d6e7b0c9789..df150557ede 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts @@ -381,7 +381,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { // MARK: implicit variable for editor selection and (tracked) whole range this._store.add(chatVariableService.registerVariable( - { name: _inlineChatContext, description: '', hidden: true }, + { id: _inlineChatContext, name: _inlineChatContext, description: '', hidden: true }, async (_message, _arg, model) => { for (const [, data] of this._sessions) { if (data.session.chatModel === model) { @@ -392,7 +392,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { } )); this._store.add(chatVariableService.registerVariable( - { name: _inlineChatDocument, description: '', hidden: true }, + { id: _inlineChatDocument, name: _inlineChatDocument, description: '', hidden: true }, async (_message, _arg, model) => { for (const [, data] of this._sessions) { if (data.session.chatModel === model) { @@ -798,6 +798,7 @@ export class AgentInlineChatProvider implements IInlineChatSessionProvider { location: ChatAgentLocation.Editor, variables: { variables: [{ + id: InlineChatContext.variableName, name: InlineChatContext.variableName, value: JSON.stringify(new InlineChatContext(request.previewDocument, request.selection, request.wholeRange)) }] diff --git a/src/vscode-dts/vscode.proposed.chatParticipant.d.ts b/src/vscode-dts/vscode.proposed.chatParticipant.d.ts index 4abc744d5f1..04f6a8460af 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipant.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipant.d.ts @@ -12,7 +12,7 @@ declare module 'vscode' { /** * The prompt as entered by the user. * - * Information about variables used in this request is stored in {@link ChatRequestTurn.variables}. + * Information about references used in this request is stored in {@link ChatRequestTurn.references}. * * *Note* that the {@link ChatParticipant.name name} of the participant and the {@link ChatCommand.name command} * are not part of the prompt. @@ -30,12 +30,11 @@ declare module 'vscode' { readonly command?: string; /** - * The variables that were used in this message. - * TODO@API rename to `references`? + * The references that were used in this message. */ - readonly variables: ChatValueReference[]; + readonly references: ChatValueReference[]; - private constructor(prompt: string, command: string | undefined, variables: ChatValueReference[], participant: string); + private constructor(prompt: string, command: string | undefined, references: ChatValueReference[], participant: string); } /** @@ -235,9 +234,14 @@ declare module 'vscode' { } export interface ChatValueReference { + /** + * A unique identifier for this reference. + */ + readonly id: string; + /** * The name of the reference. - * TODO@API How to handle name conflicts? Need id vs name? + * TODO@API should name be provided at all, or only ID? */ readonly name: string; @@ -249,6 +253,11 @@ declare module 'vscode' { */ readonly range: [start: number, end: number]; + /** + * A description of this value that could be used in an LLM prompt. + */ + readonly modelDescription?: string; + /** * The value of this reference. The `string | Uri | Location` types are used today, but this could expand in the future. */ @@ -259,7 +268,7 @@ declare module 'vscode' { /** * The prompt as entered by the user. * - * Information about variables used in this request is stored in {@link ChatRequest.variables}. + * Information about references used in this request is stored in {@link ChatRequest.references}. * * *Note* that the {@link ChatParticipant.name name} of the participant and the {@link ChatCommand.name command} * are not part of the prompt. @@ -273,15 +282,15 @@ declare module 'vscode' { /** - * The list of variables and their values that are referenced in the prompt. + * The list of references and their values that are referenced in the prompt. * * *Note* that the prompt contains varibale references as authored and that it is up to the participant * to further modify the prompt, for instance by inlining variable values or creating links to - * headings which contain the resolved values. Variables are sorted in reverse by their range + * headings which contain the resolved values. References are sorted in reverse by their range * in the prompt. That means the last variable in the prompt is the first in this list. This simplifies * string-manipulation of the prompt. */ - readonly variables: readonly ChatValueReference[]; + readonly references: readonly ChatValueReference[]; } /** diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index 1f0d3796885..159b0124fb2 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -196,6 +196,7 @@ declare module 'vscode' { } export class ChatCompletionItem { + id: string; label: string | CompletionItemLabel; values: ChatVariableValue[]; insertText?: string; @@ -203,7 +204,7 @@ declare module 'vscode' { documentation?: string | MarkdownString; command?: Command; - constructor(label: string | CompletionItemLabel, values: ChatVariableValue[]); + constructor(id: string, label: string | CompletionItemLabel, values: ChatVariableValue[]); } export type ChatExtendedRequestHandler = (request: ChatRequest, context: ChatContext, response: ChatResponseStream, token: CancellationToken) => ProviderResult; diff --git a/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts b/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts index eb6f0882d63..b22c77444bd 100644 --- a/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts +++ b/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts @@ -9,11 +9,12 @@ declare module 'vscode' { /** * Register a variable which can be used in a chat request to any participant. + * @param id A unique ID for the variable. * @param name The name of the variable, to be used in the chat input as `#name`. * @param description A description of the variable for the chat input suggest widget. * @param resolver Will be called to provide the chat variable's value when it is used. */ - export function registerChatVariableResolver(name: string, description: string, resolver: ChatVariableResolver): Disposable; + export function registerChatVariableResolver(id: string, name: string, userDescription: string, modelDescription: string | undefined, resolver: ChatVariableResolver): Disposable; } export interface ChatVariableValue { From d6b26fecc83ea37c110364caff75bb62874ca55a Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Mon, 13 May 2024 14:42:43 -0700 Subject: [PATCH 150/357] testing: fix coverCountBadgeForeground color token is not functional Fixes #212479 --- .../contrib/testing/browser/media/testing.css | 112 +++++++++--------- 1 file changed, 57 insertions(+), 55 deletions(-) diff --git a/src/vs/workbench/contrib/testing/browser/media/testing.css b/src/vs/workbench/contrib/testing/browser/media/testing.css index 3068f3db57f..0e761d95c39 100644 --- a/src/vs/workbench/contrib/testing/browser/media/testing.css +++ b/src/vs/workbench/contrib/testing/browser/media/testing.css @@ -541,65 +541,67 @@ outline-offset: -1px; } -.coverage-deco-inline.coverage-deco-hit { - background: var(--vscode-testing-coveredBackground); - outline: 1px solid var(--vscode-testing-coveredBorder); -} +.monaco-editor { + .coverage-deco-inline.coverage-deco-hit { + background: var(--vscode-testing-coveredBackground); + outline: 1px solid var(--vscode-testing-coveredBorder); + } -.coverage-deco-inline.coverage-deco-miss { - background: var(--vscode-testing-uncoveredBackground); - outline: 1px solid var(--vscode-testing-uncoveredBorder); -} + .coverage-deco-inline.coverage-deco-miss { + background: var(--vscode-testing-uncoveredBackground); + outline: 1px solid var(--vscode-testing-uncoveredBorder); + } -.hc-light .coverage-deco-inline.coverage-deco-hit, -.hc-black .coverage-deco-inline.coverage-deco-hit { - outline-style: dashed; -} + .hc-light .coverage-deco-inline.coverage-deco-hit, + .hc-black .coverage-deco-inline.coverage-deco-hit { + outline-style: dashed; + } -.coverage-deco-branch-miss-indicator { - height: 100%; - width: 4ch; - position: relative; - display: inline-block; - font: inherit !important; -} + .coverage-deco-branch-miss-indicator { + height: 100%; + width: 4ch; + position: relative; + display: inline-block; + font: inherit !important; + } -.coverage-deco-branch-miss-indicator::before { - position: absolute; - top: 50%; - left: 50%; - text-align: center; - transform: translate(-50%, -50%); - padding: calc(var(--vscode-testing-coverage-lineHeight) / 10); - border-radius: 2px; - font: normal normal normal calc(var(--vscode-testing-coverage-lineHeight) / 2)/1 codicon; - border: 1px solid; -} + .coverage-deco-branch-miss-indicator::before { + position: absolute; + top: 50%; + left: 50%; + text-align: center; + transform: translate(-50%, -50%); + padding: calc(var(--vscode-testing-coverage-lineHeight) / 10); + border-radius: 2px; + font: normal normal normal calc(var(--vscode-testing-coverage-lineHeight) / 2)/1 codicon; + border: 1px solid; + } -.coverage-deco-inline-count { - position: relative; - background: var(--vscode-testing-coverCountBadgeBackground); - color: var(--vscode-testing-coverCountBadgeForeground); - font-size: 0.7em; - margin: 0 0.7em 0 0.4em; - padding: 0.2em 0 0.2em 0.2em; - /* display: inline-block; */ - border-top-left-radius: 2px; - border-bottom-left-radius: 2px; -} + .coverage-deco-inline-count { + position: relative; + background: var(--vscode-testing-coverCountBadgeBackground); + color: var(--vscode-testing-coverCountBadgeForeground); + font-size: 0.7em; + margin: 0 0.7em 0 0.4em; + padding: 0.2em 0 0.2em 0.2em; + /* display: inline-block; */ + border-top-left-radius: 2px; + border-bottom-left-radius: 2px; -.coverage-deco-inline-count::after { - content: ''; - display: block; - position: absolute; - left: 100%; - top: 0; - bottom: 0; - width: 0.5em; - background-image: - linear-gradient(to bottom left, transparent 50%, var(--vscode-testing-coverCountBadgeBackground) 0), - linear-gradient(to bottom right, var(--vscode-testing-coverCountBadgeBackground) 50%, transparent 0); - background-size: 100% 50%; - background-repeat: no-repeat; - background-position: top, bottom; + &::after { + content: ''; + display: block; + position: absolute; + left: 100%; + top: 0; + bottom: 0; + width: 0.5em; + background-image: + linear-gradient(to bottom left, transparent 50%, var(--vscode-testing-coverCountBadgeBackground) 0), + linear-gradient(to bottom right, var(--vscode-testing-coverCountBadgeBackground) 50%, transparent 0); + background-size: 100% 50%; + background-repeat: no-repeat; + background-position: top, bottom; + } + } } From 31eadceccea619eed76cf2320db84c1e764192a1 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Mon, 13 May 2024 15:19:21 -0700 Subject: [PATCH 151/357] Make ChatValueReference#range optional (#212641) --- src/vs/workbench/api/common/extHostTypeConverters.ts | 2 +- src/vscode-dts/vscode.proposed.chatParticipant.d.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index b6591ad6f9b..25001b7bc90 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -2574,7 +2574,7 @@ export namespace ChatAgentValueReference { return { id: variable.id, name: variable.name, - range: (variable.range && [variable.range.start, variable.range.endExclusive])!, // TODO + range: variable.range && [variable.range.start, variable.range.endExclusive], value: isUriComponents(value) ? URI.revive(value) : value, modelDescription: variable.modelDescription }; diff --git a/src/vscode-dts/vscode.proposed.chatParticipant.d.ts b/src/vscode-dts/vscode.proposed.chatParticipant.d.ts index 04f6a8460af..a59b3c8d849 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipant.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipant.d.ts @@ -246,12 +246,12 @@ declare module 'vscode' { readonly name: string; /** - * The start and end index of the variable in the {@link ChatRequest.prompt prompt}. + * The start and end index of the reference in the {@link ChatRequest.prompt prompt}. When undefined, the * * *Note* that the indices take the leading `#`-character into account which means they can * used to modify the prompt as-is. */ - readonly range: [start: number, end: number]; + readonly range?: [start: number, end: number]; /** * A description of this value that could be used in an LLM prompt. @@ -285,9 +285,9 @@ declare module 'vscode' { * The list of references and their values that are referenced in the prompt. * * *Note* that the prompt contains varibale references as authored and that it is up to the participant - * to further modify the prompt, for instance by inlining variable values or creating links to + * to further modify the prompt, for instance by inlining reference values or creating links to * headings which contain the resolved values. References are sorted in reverse by their range - * in the prompt. That means the last variable in the prompt is the first in this list. This simplifies + * in the prompt. That means the last reference in the prompt is the first in this list. This simplifies * string-manipulation of the prompt. */ readonly references: readonly ChatValueReference[]; From 39fc2dfefa50eab5f03b412206909efa5469f29e Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Mon, 13 May 2024 15:26:06 -0700 Subject: [PATCH 152/357] Fixes a few more cases of #211878 (#212642) --- .../extensions/browser/languageRecommendations.ts | 4 +--- src/vs/workbench/contrib/logs/common/logsActions.ts | 4 ++-- .../contrib/markers/browser/markersViewActions.ts | 4 ++-- .../contrib/workspace/browser/workspaceTrustEditor.ts | 9 ++++++--- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/extensions/browser/languageRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/languageRecommendations.ts index ac493c927fe..a310c9f4fdb 100644 --- a/src/vs/workbench/contrib/extensions/browser/languageRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/languageRecommendations.ts @@ -20,7 +20,7 @@ export class LanguageRecommendations extends ExtensionRecommendations { protected async doActivate(): Promise { if (this.productService.languageExtensionTips) { - this._recommendations = this.productService.languageExtensionTips.map(extensionId => ({ + this._recommendations = this.productService.languageExtensionTips.map((extensionId): ExtensionRecommendation => ({ extension: extensionId.toLowerCase(), reason: { reasonId: ExtensionRecommendationReason.Application, @@ -29,6 +29,4 @@ export class LanguageRecommendations extends ExtensionRecommendations { })); } } - } - diff --git a/src/vs/workbench/contrib/logs/common/logsActions.ts b/src/vs/workbench/contrib/logs/common/logsActions.ts index 3ececbb1223..86c59a48995 100644 --- a/src/vs/workbench/contrib/logs/common/logsActions.ts +++ b/src/vs/workbench/contrib/logs/common/logsActions.ts @@ -171,7 +171,7 @@ export class OpenWindowSessionLogFileAction extends Action { override async run(): Promise { const sessionResult = await this.quickInputService.pick( - this.getSessions().then(sessions => sessions.map((s, index) => ({ + this.getSessions().then(sessions => sessions.map((s, index): IQuickPickItem => ({ id: s.toString(), label: basename(s), description: index === 0 ? nls.localize('current', "Current") : undefined @@ -182,7 +182,7 @@ export class OpenWindowSessionLogFileAction extends Action { }); if (sessionResult) { const logFileResult = await this.quickInputService.pick( - this.getLogFiles(URI.parse(sessionResult.id!)).then(logFiles => logFiles.map(s => ({ + this.getLogFiles(URI.parse(sessionResult.id!)).then(logFiles => logFiles.map((s): IQuickPickItem => ({ id: s.toString(), label: basename(s) }))), diff --git a/src/vs/workbench/contrib/markers/browser/markersViewActions.ts b/src/vs/workbench/contrib/markers/browser/markersViewActions.ts index 169dc418962..2e7f61cc621 100644 --- a/src/vs/workbench/contrib/markers/browser/markersViewActions.ts +++ b/src/vs/workbench/contrib/markers/browser/markersViewActions.ts @@ -92,7 +92,7 @@ export class MarkersFilters extends Disposable { set showErrors(showErrors: boolean) { if (this._showErrors.get() !== showErrors) { this._showErrors.set(showErrors); - this._onDidChange.fire({ showErrors: true }); + this._onDidChange.fire({ showErrors: true }); } } @@ -103,7 +103,7 @@ export class MarkersFilters extends Disposable { set showInfos(showInfos: boolean) { if (this._showInfos.get() !== showInfos) { this._showInfos.set(showInfos); - this._onDidChange.fire({ showInfos: true }); + this._onDidChange.fire({ showInfos: true }); } } diff --git a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts index 51c08c3158b..6ee778a292b 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts @@ -431,7 +431,8 @@ class TrustedUriActionsColumnRenderer implements ITableRenderer{ + return { + label: '', class: ThemeIcon.asClassName(editIcon), enabled: true, id: 'editTrustedUri', @@ -443,7 +444,8 @@ class TrustedUriActionsColumnRenderer implements ITableRenderer{ + return { + label: '', class: ThemeIcon.asClassName(folderPickerIcon), enabled: true, id: 'pickerTrustedUri', @@ -455,7 +457,8 @@ class TrustedUriActionsColumnRenderer implements ITableRenderer{ + return { + label: '', class: ThemeIcon.asClassName(removeIcon), enabled: true, id: 'deleteTrustedUri', From 358a3a65d3ed4d1fa6f86f918dcda293a9e7096e Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Mon, 13 May 2024 15:27:00 -0700 Subject: [PATCH 153/357] fix incomplete coverage reports on macos (#212639) * eng: fix incomplete coverage reports on macos Wait for stdout to drain before exiting * better fix --- test/unit/electron/index.js | 25 ++++++++++++++++--------- test/unit/fullJsonStreamReporter.js | 26 +++++++++++++++----------- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/test/unit/electron/index.js b/test/unit/electron/index.js index 908fb2de97d..09f573713a9 100644 --- a/test/unit/electron/index.js +++ b/test/unit/electron/index.js @@ -315,14 +315,18 @@ app.on('ready', () => { } }); + const reporters = []; + if (args.tfs) { - new mocha.reporters.Spec(runner); - new MochaJUnitReporter(runner, { - reporterOptions: { - testsuitesTitle: `${args.tfs} ${process.platform}`, - mochaFile: process.env.BUILD_ARTIFACTSTAGINGDIRECTORY ? path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${args.tfs.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) : undefined - } - }); + reporters.push( + new mocha.reporters.Spec(runner), + new MochaJUnitReporter(runner, { + reporterOptions: { + testsuitesTitle: `${args.tfs} ${process.platform}`, + mochaFile: process.env.BUILD_ARTIFACTSTAGINGDIRECTORY ? path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${args.tfs.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) : undefined + } + }), + ); } else { // mocha patches symbols to use windows escape codes, but it seems like // Electron mangles these in its output. @@ -334,10 +338,13 @@ app.on('ready', () => { }); } - applyReporter(runner, args); + reporters.push(applyReporter(runner, args)); } if (!args.dev) { - ipcMain.on('all done', () => app.exit(runner.didFail ? 1 : 0)); + ipcMain.on('all done', async () => { + await Promise.all(reporters.map(r => r.drain?.())); + app.exit(runner.didFail ? 1 : 0); + }); } }); diff --git a/test/unit/fullJsonStreamReporter.js b/test/unit/fullJsonStreamReporter.js index a130a36bf37..7b345ea70fb 100644 --- a/test/unit/fullJsonStreamReporter.js +++ b/test/unit/fullJsonStreamReporter.js @@ -26,15 +26,15 @@ module.exports = class FullJsonStreamReporter extends BaseRunner { super(runner, options); const total = runner.total; - runner.once(EVENT_RUN_BEGIN, () => writeEvent(['start', { total }])); - runner.once(EVENT_RUN_END, () => writeEvent(['end', this.stats])); + runner.once(EVENT_RUN_BEGIN, () => this.writeEvent(['start', { total }])); + runner.once(EVENT_RUN_END, () => this.writeEvent(['end', this.stats])); // custom coverage events: - runner.on('coverage init', (c) => writeEvent(['coverageInit', c])); - runner.on('coverage increment', (context, coverage) => writeEvent(['coverageIncrement', { ...context, coverage }])); + runner.on('coverage init', (c) => this.writeEvent(['coverageInit', c])); + runner.on('coverage increment', (context, coverage) => this.writeEvent(['coverageIncrement', { ...context, coverage }])); - runner.on(EVENT_TEST_BEGIN, test => writeEvent(['testStart', clean(test)])); - runner.on(EVENT_TEST_PASS, test => writeEvent(['pass', clean(test)])); + runner.on(EVENT_TEST_BEGIN, test => this.writeEvent(['testStart', clean(test)])); + runner.on(EVENT_TEST_PASS, test => this.writeEvent(['pass', clean(test)])); runner.on(EVENT_TEST_FAIL, (test, err) => { test = clean(test); test.actual = err.actual; @@ -44,14 +44,18 @@ module.exports = class FullJsonStreamReporter extends BaseRunner { test.snapshotPath = err.snapshotPath; test.err = err.message; test.stack = err.stack || null; - writeEvent(['fail', test]); + this.writeEvent(['fail', test]); }); } -}; -function writeEvent(event) { - process.stdout.write(JSON.stringify(event) + '\n'); -} + drain() { + return Promise.resolve(this.lastEvent); + } + + writeEvent(event) { + this.lastEvent = new Promise(r => process.stdout.write(JSON.stringify(event) + '\n', r)); + } +}; const clean = test => ({ title: test.title, From 597cd68a41a5ee0266bfe4e0665cf646292eddad Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Mon, 13 May 2024 15:43:11 -0700 Subject: [PATCH 154/357] Accept URI for reference icon (#212643) --- src/vs/workbench/api/common/extHostTypeConverters.ts | 5 +++-- src/vs/workbench/api/common/extHostTypes.ts | 4 ++-- src/vs/workbench/contrib/chat/common/chatService.ts | 2 +- src/vscode-dts/vscode.proposed.chatParticipant.d.ts | 6 +++--- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 25001b7bc90..e05504f9cd6 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -2441,8 +2441,9 @@ export namespace ChatResponseTextEditPart { export namespace ChatResponseReferencePart { export function from(part: vscode.ChatResponseReferencePart): Dto { const iconPath = ThemeIcon.isThemeIcon(part.iconPath) ? part.iconPath - : (part.iconPath && 'light' in part.iconPath && 'dark' in part.iconPath && URI.isUri(part.iconPath.light) && URI.isUri(part.iconPath.dark) ? { light: URI.revive(part.iconPath.light), dark: URI.revive(part.iconPath.dark) } - : undefined); + : URI.isUri(part.iconPath) ? { light: URI.revive(part.iconPath) } + : (part.iconPath && 'light' in part.iconPath && 'dark' in part.iconPath && URI.isUri(part.iconPath.light) && URI.isUri(part.iconPath.dark) ? { light: URI.revive(part.iconPath.light), dark: URI.revive(part.iconPath.dark) } + : undefined); if ('variableName' in part.value) { return { kind: 'reference', diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index d6f6ea737c7..b69486af399 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -4405,8 +4405,8 @@ export class ChatResponseCommandButtonPart { export class ChatResponseReferencePart { value: vscode.Uri | vscode.Location | { variableName: string; value?: vscode.Uri | vscode.Location }; - iconPath?: vscode.ThemeIcon | { light: vscode.Uri; dark: vscode.Uri }; - constructor(value: vscode.Uri | vscode.Location | { variableName: string; value?: vscode.Uri | vscode.Location }, iconPath?: vscode.ThemeIcon | { light: vscode.Uri; dark: vscode.Uri }) { + iconPath?: vscode.Uri | vscode.ThemeIcon | { light: vscode.Uri; dark: vscode.Uri }; + constructor(value: vscode.Uri | vscode.Location | { variableName: string; value?: vscode.Uri | vscode.Location }, iconPath?: vscode.Uri | vscode.ThemeIcon | { light: vscode.Uri; dark: vscode.Uri }) { this.value = value; this.iconPath = iconPath; } diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 9ffe4750693..49e6eafe9ea 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -76,7 +76,7 @@ export interface IChatContentVariableReference { export interface IChatContentReference { reference: URI | Location | IChatContentVariableReference; - iconPath?: ThemeIcon | { light: URI; dark: URI }; + iconPath?: ThemeIcon | { light: URI; dark?: URI }; kind: 'reference'; } diff --git a/src/vscode-dts/vscode.proposed.chatParticipant.d.ts b/src/vscode-dts/vscode.proposed.chatParticipant.d.ts index a59b3c8d849..34ab3c3681b 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipant.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipant.d.ts @@ -358,7 +358,7 @@ declare module 'vscode' { * @param iconPath Icon for the reference shown in UI * @returns This stream. */ - reference(value: Uri | Location | { variableName: string; value?: Uri | Location }, iconPath?: ThemeIcon | { light: Uri; dark: Uri }): ChatResponseStream; + reference(value: Uri | Location | { variableName: string; value?: Uri | Location }, iconPath?: Uri | ThemeIcon | { light: Uri; dark: Uri }): ChatResponseStream; /** * Pushes a part to this stream. @@ -401,8 +401,8 @@ declare module 'vscode' { export class ChatResponseReferencePart { value: Uri | Location | { variableName: string; value?: Uri | Location }; - iconPath?: ThemeIcon | { light: Uri; dark: Uri }; - constructor(value: Uri | Location | { variableName: string; value?: Uri | Location }, iconPath?: ThemeIcon | { light: Uri; dark: Uri }); + iconPath?: Uri | ThemeIcon | { light: Uri; dark: Uri }; + constructor(value: Uri | Location | { variableName: string; value?: Uri | Location }, iconPath?: Uri | ThemeIcon | { light: Uri; dark: Uri }); } export class ChatResponseCommandButtonPart { From bbc4ba1eff648ee542972589216f211b7a6a08f2 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Mon, 13 May 2024 20:45:26 -0700 Subject: [PATCH 155/357] Move 'variableName' form of 'reference' to chatParticipantAdditions.d.ts (#212648) Because this is confusing, and might be less relevant in the future --- src/vs/workbench/api/common/extHostChatAgents2.ts | 4 ++++ src/vs/workbench/api/common/extHostTypeConverters.ts | 4 ++-- src/vscode-dts/vscode.proposed.chatParticipant.d.ts | 6 +++--- .../vscode.proposed.chatParticipantAdditions.d.ts | 2 ++ 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 0d3c964220d..8e00dbbccd0 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -153,6 +153,10 @@ class ChatAgentResponseStream { reference(value, iconPath) { throwIfDone(this.reference); + if ('variableName' in value) { + checkProposedApiEnabled(that._extension, 'chatParticipantAdditions'); + } + if ('variableName' in value && !value.value) { // The participant used this variable. Does that variable have any references to pull in? const matchingVarData = that._request.variables.variables.find(v => v.name === value.variableName); diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index e05504f9cd6..e4031e0b167 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -2439,7 +2439,7 @@ export namespace ChatResponseTextEditPart { } export namespace ChatResponseReferencePart { - export function from(part: vscode.ChatResponseReferencePart): Dto { + export function from(part: types.ChatResponseReferencePart): Dto { const iconPath = ThemeIcon.isThemeIcon(part.iconPath) ? part.iconPath : URI.isUri(part.iconPath) ? { light: URI.revive(part.iconPath) } : (part.iconPath && 'light' in part.iconPath && 'dark' in part.iconPath && URI.isUri(part.iconPath.light) && URI.isUri(part.iconPath.dark) ? { light: URI.revive(part.iconPath.light), dark: URI.revive(part.iconPath.dark) } @@ -2478,7 +2478,7 @@ export namespace ChatResponseReferencePart { value: value.reference.value && mapValue(value.reference.value) } : mapValue(value.reference) - ); + ) as vscode.ChatResponseReferencePart; // 'value' is extended with variableName } } diff --git a/src/vscode-dts/vscode.proposed.chatParticipant.d.ts b/src/vscode-dts/vscode.proposed.chatParticipant.d.ts index 34ab3c3681b..ba74d3726de 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipant.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipant.d.ts @@ -358,7 +358,7 @@ declare module 'vscode' { * @param iconPath Icon for the reference shown in UI * @returns This stream. */ - reference(value: Uri | Location | { variableName: string; value?: Uri | Location }, iconPath?: Uri | ThemeIcon | { light: Uri; dark: Uri }): ChatResponseStream; + reference(value: Uri | Location, iconPath?: Uri | ThemeIcon | { light: Uri; dark: Uri }): ChatResponseStream; /** * Pushes a part to this stream. @@ -400,9 +400,9 @@ declare module 'vscode' { } export class ChatResponseReferencePart { - value: Uri | Location | { variableName: string; value?: Uri | Location }; + value: Uri | Location; iconPath?: Uri | ThemeIcon | { light: Uri; dark: Uri }; - constructor(value: Uri | Location | { variableName: string; value?: Uri | Location }, iconPath?: Uri | ThemeIcon | { light: Uri; dark: Uri }); + constructor(value: Uri | Location, iconPath?: Uri | ThemeIcon | { light: Uri; dark: Uri }); } export class ChatResponseCommandButtonPart { diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index 159b0124fb2..c34b97baace 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -160,6 +160,8 @@ declare module 'vscode' { */ warning(message: string | MarkdownString): ChatResponseStream; + reference(value: Uri | Location | { variableName: string; value?: Uri | Location }, iconPath?: Uri | ThemeIcon | { light: Uri; dark: Uri }): ChatResponseStream; + push(part: ExtendedChatResponsePart): ChatResponseStream; } From 47790134e69458147041764f056bdcb8b6a148f2 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Mon, 13 May 2024 23:07:31 -0700 Subject: [PATCH 156/357] feat: introduce Attach Context entrypoint in chat (#212652) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: introduce Attach Context entrypoint in chat * Clean up setting * Fix compile * 🫠 --- .../browser/actions/chatContextActions.ts | 74 ++++++++++++++++++ .../contrib/chat/browser/chat.contribution.ts | 3 + src/vs/workbench/contrib/chat/browser/chat.ts | 3 +- .../contrib/chat/browser/chatInputPart.ts | 78 ++++++++++++------- .../contrib/chat/browser/chatWidget.ts | 41 ++++------ .../browser/contrib/chatDynamicVariables.ts | 19 ++--- .../contrib/chat/browser/media/chat.css | 29 ++++++- .../contrib/chat/common/chatService.ts | 3 +- .../contrib/chat/common/chatServiceImpl.ts | 14 ++-- .../chat/test/common/mockChatService.ts | 5 +- 10 files changed, 195 insertions(+), 74 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts new file mode 100644 index 00000000000..3ff7d539025 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Codicon } from 'vs/base/common/codicons'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { URI } from 'vs/base/common/uri'; +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { localize2 } from 'vs/nls'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { AnythingQuickAccessProviderRunOptions } from 'vs/platform/quickinput/common/quickAccess'; +import { IQuickInputService, QuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { IChatWidget, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; +import { SelectAndInsertFileAction } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables'; +import { CONTEXT_IN_CHAT_INPUT } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; + +export function registerChatContextActions() { + registerAction2(AttachContextAction); +} + +class AttachContextAction extends Action2 { + + static readonly ID = 'workbench.action.chat.attachContext'; + + constructor() { + super({ + id: AttachContextAction.ID, + title: localize2('workbench.action.chat.attachContext.label', "Attach Context"), + icon: Codicon.attach, + keybinding: { + when: CONTEXT_IN_CHAT_INPUT, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyA, + weight: KeybindingWeight.EditorContrib + }, + menu: [ + { + id: MenuId.ChatExecuteSecondary, + group: 'group_1', + }, + { + id: MenuId.ChatExecute, + group: 'navigation', + }, + ] + }); + } + + override async run(accessor: ServicesAccessor, ...args: any[]): Promise { + const quickInputService = accessor.get(IQuickInputService); + const chatVariablesService = accessor.get(IChatVariablesService); + const widgetService = accessor.get(IChatWidgetService); + + const quickPickItems: QuickPickItem[] = []; + if (chatVariablesService.hasVariable(SelectAndInsertFileAction.Name)) { + quickPickItems.push(SelectAndInsertFileAction.Item, { type: 'separator' }); + } + + const picks = await quickInputService.quickAccess.pick('', { + providerOptions: { + additionPicks: quickPickItems + } + }); + + if (picks?.length) { + const context: { widget?: IChatWidget } | undefined = args[0]; + + const widget = context?.widget ?? widgetService.lastFocusedWidget; + widget?.attachContext(...picks.map((p) => ({ name: p.label, value: 'resource' in p && URI.isUri(p.resource) ? p.resource : undefined, id: p.id! }))); + } + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 8e59f6cac56..1e075c1f6a7 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -55,6 +55,7 @@ import { IVoiceChatService, VoiceChatService } from 'vs/workbench/contrib/chat/c import { IEditorResolverService, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import '../common/chatColors'; +import { registerChatContextActions } from 'vs/workbench/contrib/chat/browser/actions/chatContextActions'; // Register configuration const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); @@ -92,6 +93,7 @@ configurationRegistry.registerConfiguration({ 'chat.experimental.implicitContext': { type: 'boolean', description: nls.localize('chat.experimental.implicitContext', "Controls whether a checkbox is shown to allow the user to determine which implicit context is included with a chat participant's prompt."), + deprecated: true, default: false }, } @@ -245,6 +247,7 @@ registerQuickChatActions(); registerChatExportActions(); registerMoveActions(); registerNewChatActions(); +registerChatContextActions(); registerSingleton(IChatService, ChatService, InstantiationType.Delayed); registerSingleton(IChatWidgetService, ChatWidgetService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index d29591bc016..bfa8108369b 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -16,7 +16,7 @@ import { ChatViewPane } from 'vs/workbench/contrib/chat/browser/chatViewPane'; import { IChatWidgetContrib } from 'vs/workbench/contrib/chat/browser/chatWidget'; import { ICodeBlockActionContext } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; import { ChatAgentLocation, IChatAgentCommand, IChatAgentData } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { IChatResponseModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatRequestVariableEntry, IChatResponseModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { CHAT_PROVIDER_ID } from 'vs/workbench/contrib/chat/common/chatParticipantContribTypes'; import { IChatRequestViewModel, IChatResponseViewModel, IChatViewModel, IChatWelcomeMessageViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; @@ -157,6 +157,7 @@ export interface IChatWidget { getCodeBlockInfosForResponse(response: IChatResponseViewModel): IChatCodeBlockInfo[]; getFileTreeInfosForResponse(response: IChatResponseViewModel): IChatFileTreeInfo[]; getLastFocusedFileTreeForResponse(response: IChatResponseViewModel): IChatFileTreeInfo | undefined; + attachContext(...context: IChatRequestVariableEntry[]): void; clear(): void; } diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 211619b3336..e295612754a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -7,7 +7,7 @@ import * as dom from 'vs/base/browser/dom'; import { DEFAULT_FONT_FAMILY } from 'vs/base/browser/fonts'; import { IHistoryNavigationWidget } from 'vs/base/browser/history'; import * as aria from 'vs/base/browser/ui/aria/aria'; -import { Checkbox } from 'vs/base/browser/ui/toggle/toggle'; +import { Button } from 'vs/base/browser/ui/button/button'; import { IAction } from 'vs/base/common/actions'; import { Codicon } from 'vs/base/common/codicons'; import { Emitter } from 'vs/base/common/event'; @@ -32,14 +32,14 @@ import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { FileKind } from 'vs/platform/files/common/files'; import { registerAndCreateHistoryNavigationContext } from 'vs/platform/history/browser/contextScopedHistoryWidget'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { defaultCheckboxStyles } from 'vs/platform/theme/browser/defaultStyles'; -import { asCssVariableWithDefault, checkboxBorder, inputBackground } from 'vs/platform/theme/common/colorRegistry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { ResourceLabels } from 'vs/workbench/browser/labels'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; import { CancelAction, ChatSubmitSecondaryAgentAction, IChatExecuteActionContext, SubmitAction } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions'; @@ -47,6 +47,7 @@ import { IChatWidget } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatFollowups } from 'vs/workbench/contrib/chat/browser/chatFollowups'; import { ChatAgentLocation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { CONTEXT_CHAT_INPUT_CURSOR_AT_TOP, CONTEXT_CHAT_INPUT_HAS_FOCUS, CONTEXT_CHAT_INPUT_HAS_TEXT, CONTEXT_IN_CHAT_INPUT } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { IChatRequestVariableEntry } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChatFollowup } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { IChatHistoryEntry, IChatWidgetHistoryService } from 'vs/workbench/contrib/chat/common/chatWidgetHistoryService'; @@ -86,6 +87,16 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private _onDidAcceptFollowup = this._register(new Emitter<{ followup: IChatFollowup; response: IChatResponseViewModel | undefined }>()); readonly onDidAcceptFollowup = this._onDidAcceptFollowup.event; + private _onDidChangeAttachedContext = this._register(new Emitter()); + readonly onDidChangeAttachedContext = this._onDidChangeAttachedContext.event; + public get attachedContext() { + return this._attachedContext; + } + + private readonly _attachedContext = new Set(); + + private readonly _contextResourceLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: new Emitter().event }); + private inputEditorHeight = 0; private container!: HTMLElement; @@ -94,13 +105,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private followupsContainer!: HTMLElement; private readonly followupsDisposables = this._register(new DisposableStore()); - private implicitContextContainer!: HTMLElement; - private implicitContextLabel!: HTMLElement; - private implicitContextCheckbox!: Checkbox; - private implicitContextSettingEnabled = false; - get implicitContextEnabled() { - return this.implicitContextCheckbox.checked; - } + private attachedContextContainer!: HTMLElement; private _inputPartHeight: number = 0; get inputPartHeight() { @@ -152,15 +157,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.history = new HistoryNavigator([], 5); this._register(this.historyService.onDidClearHistory(() => this.history.clear())); - this.implicitContextSettingEnabled = this.configurationService.getValue('chat.experimental.implicitContext'); this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(AccessibilityVerbositySettingId.Chat)) { this.inputEditor.updateOptions({ ariaLabel: this._getAriaLabel() }); } - - if (e.affectsConfiguration('chat.experimental.implicitContext')) { - this.implicitContextSettingEnabled = this.configurationService.getValue('chat.experimental.implicitContext'); - } })); } @@ -270,13 +270,21 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._inputEditor.focus(); } + attachContext(...contentReferences: IChatRequestVariableEntry[]): void { + for (const reference of contentReferences) { + this.attachedContext.add(reference); + } + + this.initAttachedContext(this.attachedContextContainer); + } + render(container: HTMLElement, initialValue: string, widget: IChatWidget) { this.container = dom.append(container, $('.interactive-input-part')); this.container.classList.toggle('compact', this.options.renderStyle === 'compact'); this.followupsContainer = dom.append(this.container, $('.interactive-input-followups')); - this.implicitContextContainer = dom.append(this.container, $('.chat-implicit-context')); - this.initImplicitContext(this.implicitContextContainer); + this.attachedContextContainer = dom.append(this.container, $('.chat-attached-context')); + this.initAttachedContext(this.attachedContextContainer); const inputAndSideToolbar = dom.append(this.container, $('.interactive-input-and-side-toolbar')); const inputContainer = dom.append(inputAndSideToolbar, $('.interactive-input-and-execute-toolbar')); @@ -415,16 +423,30 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } } - private initImplicitContext(container: HTMLElement) { - this.implicitContextCheckbox = new Checkbox('#selection', true, { ...defaultCheckboxStyles, checkboxBorder: asCssVariableWithDefault(checkboxBorder, inputBackground) }); - container.append(this.implicitContextCheckbox.domNode); - this.implicitContextLabel = dom.append(container, $('span.chat-implicit-context-label')); - this.implicitContextLabel.textContent = '#selection'; - } + private initAttachedContext(container: HTMLElement) { + dom.clearNode(container); + dom.setVisibility(Boolean(this.attachedContext.size), this.attachedContextContainer); + for (const attachment of this.attachedContext) { + const widget = dom.append(container, $('.chat-attached-context-attachment.show-file-icons')); + const label = this._contextResourceLabels.create(widget, { supportIcons: true }); + const file = URI.isUri(attachment.value) ? attachment.value : attachment.value && typeof attachment.value === 'object' && 'uri' in attachment.value && URI.isUri(attachment.value.uri) ? attachment.value.uri : undefined; + if (file) { + label.setFile(file, { + fileKind: FileKind.FILE, + hidePath: true, + }); + } else { + label.setLabel(attachment.name); + } - setImplicitContextKinds(kinds: string[]) { - dom.setVisibility(this.implicitContextSettingEnabled && kinds.length > 0, this.implicitContextContainer); - this.implicitContextLabel.textContent = localize('use', "Use") + ' ' + kinds.map(k => `#${k}`).join(', '); + const clearButton = new Button(widget, { supportIcons: true }); + clearButton.icon = Codicon.close; + const disp = clearButton.onDidClick(() => { + this.attachedContext.delete(attachment); + disp.dispose(); + this._onDidChangeAttachedContext.fire(); + }); + } } async renderFollowups(items: IChatFollowup[] | undefined, response: IChatResponseViewModel | undefined): Promise { @@ -469,6 +491,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.previousInputEditorDimension = newDimension; } + this.initAttachedContext(this.attachedContextContainer); + if (allowRecurse && initialEditorScrollWidth < 10) { // This is probably the initial layout. Now that the editor is layed out with its correct width, it should report the correct contentHeight return this._layout(height, width, false); @@ -482,7 +506,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge inputPartEditorHeight: Math.min(this._inputEditor.getContentHeight(), INPUT_EDITOR_MAX_HEIGHT), inputPartHorizontalPadding: this.options.renderStyle === 'compact' ? 8 : 40, inputPartVerticalPadding: this.options.renderStyle === 'compact' ? 12 : 24, - implicitContextHeight: this.implicitContextContainer.offsetHeight, + implicitContextHeight: this.attachedContextContainer.offsetHeight, editorBorder: 2, editorPadding: 12, toolbarPadding: 4, diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index cf784a5de5f..9fa0f592933 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -33,8 +33,8 @@ import { ChatListDelegate, ChatListItemRenderer, IChatRendererDelegate } from 'v import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions'; import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { CONTEXT_CHAT_INPUT_HAS_AGENT, CONTEXT_CHAT_LOCATION, CONTEXT_CHAT_REQUEST_IN_PROGRESS, CONTEXT_IN_CHAT_SESSION, CONTEXT_RESPONSE_FILTERED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; -import { ChatModelInitState, IChatModel, IChatResponseModel } from 'vs/workbench/contrib/chat/common/chatModel'; -import { ChatRequestAgentPart, IParsedChatRequest, chatAgentLeader, chatSubcommandLeader, extractAgentAndCommand } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import { ChatModelInitState, IChatModel, IChatRequestVariableEntry, IChatResponseModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatRequestAgentPart, IParsedChatRequest, chatAgentLeader, chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; import { IChatFollowup, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; @@ -578,11 +578,11 @@ export class ChatWidget extends Disposable implements IChatWidget { } this._onDidChangeContentHeight.fire(); })); - this._register(this.inputEditor.onDidChangeModelContent(() => this.updateImplicitContextKinds())); - this._register(this.chatAgentService.onDidChangeAgents(() => { - if (this.viewModel) { - this.updateImplicitContextKinds(); + this._register(this.inputPart.onDidChangeAttachedContext(() => { + if (this.bodyDimension) { + this.layout(this.bodyDimension.height, this.bodyDimension.width); } + this._onDidChangeContentHeight.fire(); })); } @@ -592,23 +592,6 @@ export class ChatWidget extends Disposable implements IChatWidget { this.container.style.setProperty('--vscode-chat-list-background', this.themeService.getColorTheme().getColor(this.styles.listBackground)?.toString() ?? ''); } - private updateImplicitContextKinds() { - if (!this.viewModel) { - return; - } - this.parsedChatRequest = undefined; - const agentAndSubcommand = extractAgentAndCommand(this.parsedInput); - const currentAgent = agentAndSubcommand.agentPart?.agent ?? this.chatAgentService.getDefaultAgent(this.location); - const implicitVariables = agentAndSubcommand.commandPart ? - agentAndSubcommand.commandPart.command.defaultImplicitVariables : - currentAgent?.defaultImplicitVariables; - this.inputPart.setImplicitContextKinds(implicitVariables ?? []); - - if (this.bodyDimension) { - this.layout(this.bodyDimension.height, this.bodyDimension.width); - } - } - setModel(model: IChatModel, viewState: IChatViewState): void { if (!this.container) { throw new Error('Call render() before setModel()'); @@ -651,7 +634,6 @@ export class ChatWidget extends Disposable implements IChatWidget { revealLastElement(this.tree); } - this.updateImplicitContextKinds(); } getFocus(): ChatTreeItem | undefined { @@ -721,7 +703,8 @@ 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, { implicitVariablesEnabled: this.inputPart.implicitContextEnabled, location: this.location, parserContext: { selectedAgent: this._lastSelectedAgent } }); + const result = await this.chatService.sendRequest(this.viewModel.sessionId, input, { implicitVariablesEnabled: false, location: this.location, parserContext: { selectedAgent: this._lastSelectedAgent }, attachedContext: [...this.inputPart.attachedContext.values()] }); + this.inputPart.attachedContext.clear(); if (result) { const inputState = this.collectInputState(); @@ -738,6 +721,14 @@ export class ChatWidget extends Disposable implements IChatWidget { return undefined; } + + attachContext(...contentReferences: IChatRequestVariableEntry[]) { + this.inputPart.attachContext(...contentReferences); + if (this.bodyDimension) { + this.layout(this.bodyDimension.height, this.bodyDimension.width); + } + } + getCodeBlockInfosForResponse(response: IChatResponseViewModel): IChatCodeBlockInfo[] { return this.renderer.getCodeBlockInfosForResponse(response); } diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts index 3f0f72edd27..3b587a6d6bd 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts @@ -127,6 +127,12 @@ function isSelectAndInsertFileActionContext(context: any): context is SelectAndI } export class SelectAndInsertFileAction extends Action2 { + static readonly Name = 'files'; + static readonly Item = { + label: localize('allFiles', 'All Files'), + description: localize('allFilesDescription', 'Search for relevant files in the workspace and provide context from them'), + id: 'vscode.file' + }; static readonly ID = 'workbench.action.chat.selectAndInsertFile'; constructor() { @@ -153,18 +159,13 @@ export class SelectAndInsertFileAction extends Action2 { }; let options: IQuickAccessOptions | undefined; - const filesVariableName = 'files'; - const filesItem = { - label: localize('allFiles', 'All Files'), - description: localize('allFilesDescription', 'Search for relevant files in the workspace and provide context from them'), - }; // If we have a `files` variable, add an option to select all files in the picker. // This of course assumes that the `files` variable has the behavior that it searches // through files in the workspace. - if (chatVariablesService.hasVariable(filesVariableName)) { + if (chatVariablesService.hasVariable(SelectAndInsertFileAction.Name)) { options = { providerOptions: { - additionPicks: [filesItem, { type: 'separator' }] + additionPicks: [SelectAndInsertFileAction.Item, { type: 'separator' }] }, }; } @@ -180,8 +181,8 @@ export class SelectAndInsertFileAction extends Action2 { const range = context.range; // Handle the special case of selecting all files - if (picks[0] === filesItem) { - const text = `#${filesVariableName}`; + if (picks[0] === SelectAndInsertFileAction.Item) { + const text = `#${SelectAndInsertFileAction.Name}`; const success = editor.executeEdits('chatInsertFile', [{ range, text: text + ' ' }]); if (!success) { logService.trace(`SelectAndInsertFileAction: failed to insert "${text}"`); diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 5f52c85cbde..95935ccc497 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -469,11 +469,36 @@ padding: 6px 0px; } -.interactive-session .chat-implicit-context { +.interactive-session .chat-attached-context .chat-attached-context-attachment { + display: flex; + gap: 4px; +} + +.interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-button:hover { + cursor: pointer; +} + +.interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-button { + display: flex; + align-items: center; +} + +.interactive-session .chat-attached-context { padding: 8px 8px 13px; - margin-bottom: -5px; + margin-right: -3px; + margin-bottom: -4px; border: 1px solid var(--vscode-input-border, var(--vscode-input-background, transparent)); border-radius: 6px 6px 0px 0px; + display: flex; + gap: 4px; + flex-wrap: wrap; +} + +.interactive-session .chat-attached-context .chat-attached-context-attachment { + padding: 3px; + border: 1px solid var(--vscode-input-border, var(--vscode-input-background, transparent)); + border-radius: 5px; + height: 20px; } .interactive-session-followups { diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 49e6eafe9ea..c9773981e4f 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -14,7 +14,7 @@ import { Command, Location, TextEdit } from 'vs/editor/common/languages'; import { FileType } from 'vs/platform/files/common/files'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { ChatModel, IChatModel, IChatRequestModel, IChatRequestVariableData, IChatResponseModel, IExportableChatData, ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatModel, IChatModel, IChatRequestModel, IChatRequestVariableData, IChatRequestVariableEntry, IChatResponseModel, IExportableChatData, 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'; @@ -306,6 +306,7 @@ export interface IChatSendRequestOptions { noCommandDetection?: boolean; acceptedConfirmationData?: any[]; rejectedConfirmationData?: any[]; + attachedContext?: IChatRequestVariableEntry[]; /** The target agent ID can be specified with this property instead of using @ in 'message' */ agentId?: string; diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index 821e0b6875d..59868a8097f 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -125,11 +125,6 @@ type ChatTerminalClassification = { comment: 'Provides insight into the usage of Chat features.'; }; -interface IRequestConfirmationData { - acceptedConfirmationData?: any[]; - rejectedConfirmationData?: any[]; -} - const maxPersistedSessions = 25; export class ChatService extends Disposable implements IChatService { @@ -436,7 +431,7 @@ export class ChatService extends Disposable implements IChatService { this.removeRequest(model.sessionId, request.id); - await this._sendRequestAsync(model, model.sessionId, request.message, attempt, enableCommandDetection, implicitVariablesEnabled, defaultAgent, location).responseCompletePromise; + await this._sendRequestAsync(model, model.sessionId, request.message, attempt, enableCommandDetection, implicitVariablesEnabled, defaultAgent, location, options); } async sendRequest(sessionId: string, request: string, options?: IChatSendRequestOptions): Promise { @@ -500,7 +495,7 @@ export class ChatService extends Disposable implements IChatService { return newTokenSource.token; } - private _sendRequestAsync(model: ChatModel, sessionId: string, parsedRequest: IParsedChatRequest, attempt: number, enableCommandDetection: boolean, implicitVariablesEnabled: boolean, defaultAgent: IChatAgent, location: ChatAgentLocation, confirmData?: IRequestConfirmationData): IChatSendRequestResponseState { + private _sendRequestAsync(model: ChatModel, sessionId: string, parsedRequest: IParsedChatRequest, attempt: number, enableCommandDetection: boolean, implicitVariablesEnabled: boolean, defaultAgent: IChatAgent, location: ChatAgentLocation, options?: IChatSendRequestOptions): IChatSendRequestResponseState { const followupsCancelToken = this.refreshFollowupsCancellationToken(sessionId); let request: ChatRequestModel; const agentPart = 'kind' in parsedRequest ? undefined : parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart); @@ -570,6 +565,9 @@ export class ChatService extends Disposable implements IChatService { request = model.addRequest(parsedRequest, initVariableData, attempt, agent, agentSlashCommandPart?.command); completeResponseCreated(); const variableData = await this.chatVariablesService.resolveVariables(parsedRequest, model, progressCallback, token); + if (options?.attachedContext) { + variableData.variables.push(...options.attachedContext); + } request.variableData = variableData; const promptTextResult = getPromptText(request.message); @@ -597,7 +595,7 @@ export class ChatService extends Disposable implements IChatService { enableCommandDetection, attempt, location, - ...confirmData + ...options }; const agentResult = await this.chatAgentService.invokeAgent(agent.id, requestProps, progressCallback, history, token); diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts index f2671c73f59..c432e2e2cb5 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts @@ -9,7 +9,7 @@ import { URI } from 'vs/base/common/uri'; import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatModel, IChatModel, IChatRequestModel, IChatRequestVariableData, ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { IChatCompleteResponse, IChatDetail, IChatProviderInfo, IChatSendRequestData, IChatSendRequestOptions, IChatService, IChatTransferredSessionData, IChatUserActionEvent } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatCompleteResponse, IChatContentVariableReference, IChatDetail, IChatProviderInfo, IChatSendRequestData, IChatSendRequestOptions, IChatService, IChatTransferredSessionData, IChatUserActionEvent } from 'vs/workbench/contrib/chat/common/chatService'; export class MockChatService implements IChatService { _serviceBrand: undefined; @@ -77,4 +77,7 @@ export class MockChatService implements IChatService { transferChatSession(transferredSessionData: IChatTransferredSessionData, toWorkspace: URI): void { throw new Error('Method not implemented.'); } + attachContext(...context: IChatContentVariableReference[]): void { + return; + } } From 743d036d66778d17eb1240dcb110d48e3087d324 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 14 May 2024 09:50:11 +0200 Subject: [PATCH 157/357] fix #185186 (#212668) --- .../userDataProfile/browser/extensionsResource.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/services/userDataProfile/browser/extensionsResource.ts b/src/vs/workbench/services/userDataProfile/browser/extensionsResource.ts index 5b228f19992..1fd2767bf04 100644 --- a/src/vs/workbench/services/userDataProfile/browser/extensionsResource.ts +++ b/src/vs/workbench/services/userDataProfile/browser/extensionsResource.ts @@ -194,7 +194,7 @@ export class ExtensionsResource implements IProfileResource { async getLocalExtensions(profile: IUserDataProfile): Promise { return this.withProfileScopedServices(profile, async (extensionEnablementService) => { - const result: Array = []; + const result = new Map(); const installedExtensions = await this.extensionManagementService.getInstalled(undefined, profile.extensionsResource); const disabledExtensions = extensionEnablementService.getDisabledExtensions(); for (const extension of installedExtensions) { @@ -210,6 +210,11 @@ export class ExtensionsResource implements IProfileResource { continue; } } + const existing = result.get(identifier.id.toLowerCase()); + if (existing?.disabled) { + // Remove the duplicate disabled extension + result.delete(identifier.id.toLowerCase()); + } const profileExtension: IProfileExtension = { identifier, displayName: extension.manifest.displayName }; if (disabled) { profileExtension.disabled = true; @@ -220,9 +225,9 @@ export class ExtensionsResource implements IProfileResource { if (!profileExtension.version && preRelease) { profileExtension.preRelease = true; } - result.push(profileExtension); + result.set(profileExtension.identifier.id.toLowerCase(), profileExtension); } - return result; + return [...result.values()]; }); } From 49bac27eb06c1ace8809f1f97f37078ccb105180 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Tue, 14 May 2024 10:14:39 +0200 Subject: [PATCH 158/357] Update language model metadata and add logging (#212670) * add logging when language model chat comes and goes and when selection happens * have `max{Input|Output}Token` instead of contextSize. This reflect LM model reality better --- .../api/common/extHostLanguageModels.ts | 10 ++--- .../contrib/chat/common/languageModels.ts | 15 ++++++-- .../vscode.proposed.chatProvider.d.ts | 7 ++++ .../vscode.proposed.languageModels.d.ts | 37 +++++++++++++------ 4 files changed, 48 insertions(+), 21 deletions(-) diff --git a/src/vs/workbench/api/common/extHostLanguageModels.ts b/src/vs/workbench/api/common/extHostLanguageModels.ts index 1953fccb277..c7b9c7f597b 100644 --- a/src/vs/workbench/api/common/extHostLanguageModels.ts +++ b/src/vs/workbench/api/common/extHostLanguageModels.ts @@ -159,7 +159,8 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { name: metadata.name ?? '', family: metadata.family ?? '', version: metadata.version, - tokens: metadata.tokens, + maxInputTokens: metadata.maxInputTokens ?? metadata.tokens, + maxOutputTokens: metadata.maxOutputTokens ?? metadata.tokens, auth, targetExtensions: metadata.extensions }); @@ -261,7 +262,8 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { family: data.metadata.family, version: data.metadata.version, name: data.metadata.name, - contextSize: data.metadata.tokens, + maxInputTokens: data.metadata.maxInputTokens, + maxOutputTokens: data.metadata.maxOutputTokens, countTokens(text, token) { if (!that._allLanguageModelData.has(identifier)) { throw extHostTypes.LanguageModelError.NotFound(identifier); @@ -283,10 +285,6 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { result.push(apiObject); } - if (result.length === 0) { - return undefined; - } - return result; } diff --git a/src/vs/workbench/contrib/chat/common/languageModels.ts b/src/vs/workbench/contrib/chat/common/languageModels.ts index d0021ad3e9f..bd16b27c47f 100644 --- a/src/vs/workbench/contrib/chat/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/common/languageModels.ts @@ -12,6 +12,7 @@ import { isFalsyOrWhitespace } from 'vs/base/common/strings'; import { localize } from 'vs/nls'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ILogService } from 'vs/platform/log/common/log'; import { IProgress } from 'vs/platform/progress/common/progress'; import { IExtensionService, isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; @@ -40,7 +41,8 @@ export interface ILanguageModelChatMetadata { readonly vendor: string; readonly version: string; readonly family: string; - readonly tokens: number; + readonly maxInputTokens: number; + readonly maxOutputTokens: number; readonly targetExtensions?: string[]; readonly auth?: { @@ -139,6 +141,7 @@ export class LanguageModelsService implements ILanguageModelsService { constructor( @IExtensionService private readonly _extensionService: IExtensionService, + @ILogService private readonly _logService: ILogService, ) { languageModelExtensionPoint.setHandler((extensions) => { @@ -230,14 +233,17 @@ export class LanguageModelsService implements ILanguageModelsService { } } + this._logService.trace('[LM] selected language models', selector, result); + return result; } registerLanguageModelChat(identifier: string, provider: ILanguageModelChat): IDisposable { + + this._logService.trace('[LM] registering language model chat', identifier, provider.metadata); + if (!this._vendors.has(provider.metadata.vendor)) { - // throw new Error(`Chat response provider uses UNKNOWN vendor ${provider.metadata.vendor}.`); - console.warn('USING UNKNOWN vendor', provider.metadata.vendor); - this._vendors.add(provider.metadata.vendor); + throw new Error(`Chat response provider uses UNKNOWN vendor ${provider.metadata.vendor}.`); } if (this._providers.has(identifier)) { throw new Error(`Chat response provider with identifier ${identifier} is already registered.`); @@ -247,6 +253,7 @@ export class LanguageModelsService implements ILanguageModelsService { return toDisposable(() => { if (this._providers.delete(identifier)) { this._onDidChangeProviders.fire({ removed: [identifier] }); + this._logService.trace('[LM] UNregistered language model chat', identifier, provider.metadata); } }); } diff --git a/src/vscode-dts/vscode.proposed.chatProvider.d.ts b/src/vscode-dts/vscode.proposed.chatProvider.d.ts index de344194cfc..1939929db22 100644 --- a/src/vscode-dts/vscode.proposed.chatProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatProvider.d.ts @@ -44,6 +44,13 @@ declare module 'vscode' { */ readonly version: string; + readonly maxInputTokens: number; + + readonly maxOutputTokens: number; + + /** + * @deprecated + */ tokens: number; /** diff --git a/src/vscode-dts/vscode.proposed.languageModels.d.ts b/src/vscode-dts/vscode.proposed.languageModels.d.ts index 17115a67305..c7600dc663b 100644 --- a/src/vscode-dts/vscode.proposed.languageModels.d.ts +++ b/src/vscode-dts/vscode.proposed.languageModels.d.ts @@ -133,11 +133,15 @@ declare module 'vscode' { */ readonly version: string; - // TODO@API - // max_prompt_tokens vs output_tokens vs context_size - // readonly inputTokens: number; - // readonly outputTokens: number; - readonly contextSize: number; + /** + * The maximum number of tokens that can be sent to the model in a single request. + */ + readonly maxInputTokens: number; + + /** + * The maximum number of tokens that a model can generate in a single response. + */ + readonly maxOutputTokens: number; /** * Make a chat request using a language model. @@ -273,14 +277,25 @@ declare module 'vscode' { * Select chat models by a {@link LanguageModelChatSelector selector}. This can yield in multiple or no chat models and * extensions must handle these cases, esp. when no chat model exists, gracefully. * - * *Note* that extensions can hold-on to the results returned by this function and use them later. However, whenever the + * ```ts + * + * const models = await vscode.lm.selectChatModels({family: 'gpt-3.5-turbo'})!; + * if (models.length > 0) { + * const [first] = models; + * const response = await first.sendRequest(...) + * // ... + * } else { + * // NO chat models available + * } + * ``` + * + * *Note* that extensions can hold-on to the results returned by this function and use them later. However, when the * {@link onDidChangeChatModels}-event is fired the list of chat models might have changed and extensions should re-query. * * @param selector A chat model selector. When omitted all chat models are returned. - * @returns An array of chat models or `undefined` when no chat model was selected. + * @returns An array of chat models, can be empty! */ - // TODO@API no undefined but empty array - export function selectChatModels(selector?: LanguageModelChatSelector): Thenable; + export function selectChatModels(selector?: LanguageModelChatSelector): Thenable; } /** @@ -296,9 +311,9 @@ declare module 'vscode' { /** * Checks if a request can be made to a language model. * - * *Note* that calling this function will not trigger a consent UI but just checks. + * *Note* that calling this function will not trigger a consent UI but just checks for a persisted state. * - * @param languageModelId A language model identifier, see {@link LanguageModelChat.id} + * @param chat A language model chat object. * @return `true` if a request can be made, `false` if not, `undefined` if the language * model does not exist or consent hasn't been asked for. */ From 7aa7be5f5c875f029d9e933b7c72b67798b53c0d Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 14 May 2024 10:19:21 +0200 Subject: [PATCH 159/357] voice - cleanup voice chat actions (#212653) --- .../contrib/chat/browser/chat.contribution.ts | 2 +- src/vs/workbench/contrib/chat/browser/chat.ts | 1 + .../contrib/chat/browser/chatEditor.ts | 6 + .../contrib/chat/browser/chatWidget.ts | 6 + .../{voiceChat.ts => voiceChatService.ts} | 37 +- .../actions/voiceChatActions.ts | 542 +++++++----------- .../electron-sandbox/chat.contribution.ts | 7 +- ...eChat.test.ts => voiceChatService.test.ts} | 5 +- .../browser/inlineChatController.ts | 39 +- .../contrib/speech/common/speechService.ts | 6 +- .../chat/browser/terminalChatController.ts | 2 +- .../chat/browser/terminalChatWidget.ts | 6 +- 12 files changed, 280 insertions(+), 379 deletions(-) rename src/vs/workbench/contrib/chat/common/{voiceChat.ts => voiceChatService.ts} (86%) rename src/vs/workbench/contrib/chat/test/common/{voiceChat.test.ts => voiceChatService.test.ts} (98%) diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 1e075c1f6a7..93fd6755645 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -51,7 +51,7 @@ import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVari import { ChatWidgetHistoryService, IChatWidgetHistoryService } from 'vs/workbench/contrib/chat/common/chatWidgetHistoryService'; import { ILanguageModelsService, LanguageModelsService } from 'vs/workbench/contrib/chat/common/languageModels'; import { ILanguageModelStatsService, LanguageModelStatsService } from 'vs/workbench/contrib/chat/common/languageModelStats'; -import { IVoiceChatService, VoiceChatService } from 'vs/workbench/contrib/chat/common/voiceChat'; +import { IVoiceChatService, VoiceChatService } from 'vs/workbench/contrib/chat/common/voiceChatService'; import { IEditorResolverService, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import '../common/chatColors'; diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index bfa8108369b..27edc8eca4b 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -128,6 +128,7 @@ export type IChatWidgetViewContext = IChatViewViewContext | IChatResourceViewCon export interface IChatWidget { readonly onDidChangeViewModel: Event; readonly onDidAcceptInput: Event; + readonly onDidHideInput: Event; readonly onDidSubmitAgent: Event<{ agent: IChatAgentData; slashCommand?: IChatAgentCommand }>; readonly onDidChangeParsedInput: Event; readonly location: ChatAgentLocation; diff --git a/src/vs/workbench/contrib/chat/browser/chatEditor.ts b/src/vs/workbench/contrib/chat/browser/chatEditor.ts index 875863c2e73..1da5d57b810 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditor.ts @@ -75,6 +75,12 @@ export class ChatEditor extends EditorPane { this.widget.setVisible(true); } + protected override setEditorVisible(visible: boolean): void { + super.setEditorVisible(visible); + + this.widget?.setVisible(visible); + } + public override focus(): void { super.focus(); diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 9fa0f592933..a3c600ca145 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -96,6 +96,9 @@ export class ChatWidget extends Disposable implements IChatWidget { private _onDidAcceptInput = this._register(new Emitter()); readonly onDidAcceptInput = this._onDidAcceptInput.event; + private _onDidHideInput = this._register(new Emitter()); + readonly onDidHideInput = this._onDidHideInput.event; + private _onDidChangeParsedInput = this._register(new Emitter()); readonly onDidChangeParsedInput = this._onDidChangeParsedInput.event; @@ -388,6 +391,7 @@ export class ChatWidget extends Disposable implements IChatWidget { } setVisible(visible: boolean): void { + const wasVisible = this._visible; this._visible = visible; this.visibleChangeCount++; this.renderer.setVisible(visible); @@ -400,6 +404,8 @@ export class ChatWidget extends Disposable implements IChatWidget { this.onDidChangeItems(true); } }, 0)); + } else if (wasVisible) { + this._onDidHideInput.fire(); } } diff --git a/src/vs/workbench/contrib/chat/common/voiceChat.ts b/src/vs/workbench/contrib/chat/common/voiceChatService.ts similarity index 86% rename from src/vs/workbench/contrib/chat/common/voiceChat.ts rename to src/vs/workbench/contrib/chat/common/voiceChatService.ts index 67989f188a0..ac140ad3c6b 100644 --- a/src/vs/workbench/contrib/chat/common/voiceChat.ts +++ b/src/vs/workbench/contrib/chat/common/voiceChatService.ts @@ -3,10 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from 'vs/nls'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { rtrim } from 'vs/base/common/strings'; +import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; @@ -58,6 +60,8 @@ enum PhraseTextType { AGENT_AND_COMMAND = 3 } +export const VoiceChatInProgress = new RawContextKey('voiceChatInProgress', false, { type: 'boolean', description: localize('voiceChatInProgress', "A speech-to-text session is in progress for chat.") }); + export class VoiceChatService extends Disposable implements IVoiceChatService { readonly _serviceBrand: undefined; @@ -77,9 +81,13 @@ export class VoiceChatService extends Disposable implements IVoiceChatService { private static readonly CHAT_AGENT_ALIAS = new Map([['vscode', 'code']]); + private readonly voiceChatInProgress = VoiceChatInProgress.bindTo(this.contextKeyService); + private activeVoiceChatSessions = 0; + constructor( @ISpeechService private readonly speechService: ISpeechService, - @IChatAgentService private readonly chatAgentService: IChatAgentService + @IChatAgentService private readonly chatAgentService: IChatAgentService, + @IContextKeyService private readonly contextKeyService: IContextKeyService ) { super(); } @@ -116,7 +124,19 @@ export class VoiceChatService extends Disposable implements IVoiceChatService { async createVoiceChatSession(token: CancellationToken, options: IVoiceChatSessionOptions): Promise { const disposables = new DisposableStore(); - disposables.add(token.onCancellationRequested(() => disposables.dispose())); + + const onSessionStoppedOrCanceled = (dispose: boolean) => { + this.activeVoiceChatSessions--; + if (this.activeVoiceChatSessions === 0) { + this.voiceChatInProgress.reset(); + } + + if (dispose) { + disposables.dispose(); + } + }; + + disposables.add(token.onCancellationRequested(() => onSessionStoppedOrCanceled(true))); let detectedAgent = false; let detectedSlashCommand = false; @@ -124,6 +144,10 @@ export class VoiceChatService extends Disposable implements IVoiceChatService { const emitter = disposables.add(new Emitter()); const session = await this.speechService.createSpeechToTextSession(token, 'chat'); + if (token.isCancellationRequested) { + onSessionStoppedOrCanceled(true); + } + const phrases = this.createPhrases(options.model); disposables.add(session.onDidChange(e => { switch (e.status) { @@ -193,6 +217,15 @@ export class VoiceChatService extends Disposable implements IVoiceChatService { break; } } + case SpeechToTextStatus.Started: + this.activeVoiceChatSessions++; + this.voiceChatInProgress.set(true); + emitter.fire(e); + break; + case SpeechToTextStatus.Stopped: + onSessionStoppedOrCanceled(false); + emitter.fire(e); + break; default: emitter.fire(e); break; diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts index 3a1eede292f..8b1423ae64c 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts @@ -11,7 +11,6 @@ import { Color } from 'vs/base/common/color'; import { Event } from 'vs/base/common/event'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { ThemeIcon } from 'vs/base/common/themables'; import { isNumber } from 'vs/base/common/types'; import { getCodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; @@ -36,12 +35,12 @@ import { ACTIVITY_BAR_BADGE_BACKGROUND } from 'vs/workbench/common/theme'; import { AccessibilityVoiceSettingId, SpeechTimeoutDefault, accessibilityConfigurationNodeBase } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; import { IChatExecuteActionContext } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions'; -import { CHAT_VIEW_ID, IChatWidget, IChatWidgetService, IQuickChatService, showChatView } from 'vs/workbench/contrib/chat/browser/chat'; +import { IChatWidget, IChatWidgetService, IQuickChatService, showChatView } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatAgentLocation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { CONTEXT_CHAT_REQUEST_IN_PROGRESS, CONTEXT_IN_CHAT_INPUT, CONTEXT_CHAT_ENABLED, CONTEXT_RESPONSE, CONTEXT_RESPONSE_FILTERED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; -import { IChatService, KEYWORD_ACTIVIATION_SETTING_ID } from 'vs/workbench/contrib/chat/common/chatService'; +import { KEYWORD_ACTIVIATION_SETTING_ID } from 'vs/workbench/contrib/chat/common/chatService'; import { isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; -import { IVoiceChatService } from 'vs/workbench/contrib/chat/common/voiceChat'; +import { IVoiceChatService, VoiceChatInProgress as GlobalVoiceChatInProgress } from 'vs/workbench/contrib/chat/common/voiceChatService'; import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; import { InlineChatController } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; @@ -59,21 +58,20 @@ import { IAccessibilityService } from 'vs/platform/accessibility/common/accessib //#region Speech to Text -const CONTEXT_VOICE_CHAT_GETTING_READY = new RawContextKey('voiceChatGettingReady', false, { type: 'boolean', description: localize('voiceChatGettingReady', "True when getting ready for receiving voice input from the microphone for voice chat.") }); -const CONTEXT_VOICE_CHAT_IN_PROGRESS = new RawContextKey('voiceChatInProgress', false, { type: 'boolean', description: localize('voiceChatInProgress', "True when voice recording from microphone is in progress for voice chat.") }); +type VoiceChatSessionContext = 'view' | 'inline' | 'terminal' | 'quick' | 'editor'; +const VoiceChatSessionContexts: VoiceChatSessionContext[] = ['view', 'inline', 'terminal', 'quick', 'editor']; -const CONTEXT_QUICK_VOICE_CHAT_IN_PROGRESS = new RawContextKey('quickVoiceChatInProgress', false, { type: 'boolean', description: localize('quickVoiceChatInProgress', "True when voice recording from microphone is in progress for quick chat.") }); -const CONTEXT_INLINE_VOICE_CHAT_IN_PROGRESS = new RawContextKey('inlineVoiceChatInProgress', false, { type: 'boolean', description: localize('inlineVoiceChatInProgress', "True when voice recording from microphone is in progress for inline chat.") }); -const CONTEXT_VOICE_CHAT_IN_TERMINAL_IN_PROGRESS = new RawContextKey('voiceChatInTerminalInProgress', false, { type: 'boolean', description: localize('voiceChatInTerminalInProgress', "True when voice recording from microphone is in progress for terminal chat.") }); -const CONTEXT_VOICE_CHAT_IN_VIEW_IN_PROGRESS = new RawContextKey('voiceChatInViewInProgress', false, { type: 'boolean', description: localize('voiceChatInViewInProgress', "True when voice recording from microphone is in progress in the chat view.") }); -const CONTEXT_VOICE_CHAT_IN_EDITOR_IN_PROGRESS = new RawContextKey('voiceChatInEditorInProgress', false, { type: 'boolean', description: localize('voiceChatInEditorInProgress', "True when voice recording from microphone is in progress in the chat editor.") }); +const TerminalChatExecute = MenuId.for('terminalChatInput'); // unfortunately, terminal decided to go with their own menu (https://github.com/microsoft/vscode/issues/208789) +// Global Context Keys (set on global context key service) const CanVoiceChat = ContextKeyExpr.and(CONTEXT_CHAT_ENABLED, HasSpeechProvider); const FocusInChatInput = ContextKeyExpr.or(CTX_INLINE_CHAT_FOCUSED, CONTEXT_IN_CHAT_INPUT); - const AnyChatRequestInProgress = ContextKeyExpr.or(CONTEXT_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST, TerminalChatContextKeys.requestActive); -type VoiceChatSessionContext = 'inline' | 'terminal' | 'quick' | 'view' | 'editor'; +// Scoped Context Keys (set on per-chat-context scoped context key service) +const SCOPED_VOICE_CHAT_GETTING_READY = new RawContextKey('scopedVoiceChatGettingReady', false, { type: 'boolean', description: localize('scopedVoiceChatGettingReady', "True when getting ready for receiving voice input from the microphone for voice chat. This key is only defined scoped, per chat context.") }); +const SCOPED_VOICE_CHAT_IN_PROGRESS = new RawContextKey('scopedVoiceChatInProgress', undefined, { type: 'string', description: localize('scopedVoiceChatInProgress', "Defined as a location where voice recording from microphone is in progress for voice chat. This key is only defined scoped, per chat context.") }); +const ScopedVoiceChatInProgress = ContextKeyExpr.or(...VoiceChatSessionContexts.map(context => SCOPED_VOICE_CHAT_IN_PROGRESS.isEqualTo(context))); enum VoiceChatSessionState { Stopped = 1, @@ -84,7 +82,7 @@ enum VoiceChatSessionState { interface IVoiceChatSessionController { readonly onDidAcceptInput: Event; - readonly onDidCancelInput: Event; + readonly onDidHideInput: Event; readonly context: VoiceChatSessionContext; @@ -101,214 +99,135 @@ interface IVoiceChatSessionController { class VoiceChatSessionControllerFactory { - static create(accessor: ServicesAccessor, context: 'inline'): Promise; - static create(accessor: ServicesAccessor, context: 'quick'): Promise; - static create(accessor: ServicesAccessor, context: 'view'): Promise; - static create(accessor: ServicesAccessor, context: 'terminal'): Promise; - static create(accessor: ServicesAccessor, context: 'focused'): Promise; - static create(accessor: ServicesAccessor, context: 'inline' | 'quick' | 'view' | 'terminal' | 'focused'): Promise; - static async create(accessor: ServicesAccessor, context: 'inline' | 'quick' | 'view' | 'terminal' | 'focused'): Promise { + static async create(accessor: ServicesAccessor, context: 'view' | 'inline' | 'quick' | 'focused'): Promise { const chatWidgetService = accessor.get(IChatWidgetService); - const viewsService = accessor.get(IViewsService); const quickChatService = accessor.get(IQuickChatService); const layoutService = accessor.get(IWorkbenchLayoutService); const editorService = accessor.get(IEditorService); const terminalService = accessor.get(ITerminalService); - - // Currently Focused Context - if (context === 'focused') { - - // Try with the terminal chat - const activeInstance = terminalService.activeInstance; - if (activeInstance) { - const terminalChat = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); - if (terminalChat?.hasFocus()) { - return VoiceChatSessionControllerFactory.doCreateForTerminalChat(terminalChat); - } - } - - // Try with the chat widget service, which currently - // only supports the chat view and quick chat - // https://github.com/microsoft/vscode/issues/191191 - const chatInput = chatWidgetService.lastFocusedWidget; - if (chatInput?.hasInputFocus()) { - // Unfortunately there does not seem to be a better way - // to figure out if the chat widget is in a part or picker - if ( - layoutService.hasFocus(Parts.SIDEBAR_PART) || - layoutService.hasFocus(Parts.PANEL_PART) || - layoutService.hasFocus(Parts.AUXILIARYBAR_PART) - ) { - return VoiceChatSessionControllerFactory.doCreateForChatView(chatInput, viewsService); - } - - if (layoutService.hasFocus(Parts.EDITOR_PART)) { - return VoiceChatSessionControllerFactory.doCreateForChatEditor(chatInput, viewsService); - } - - return VoiceChatSessionControllerFactory.doCreateForQuickChat(chatInput, quickChatService); - } - - // Try with the inline chat - const activeCodeEditor = getCodeEditor(editorService.activeTextEditorControl); - if (activeCodeEditor) { - const inlineChat = InlineChatController.get(activeCodeEditor); - if (inlineChat?.hasFocus()) { - return VoiceChatSessionControllerFactory.doCreateForInlineChat(inlineChat); - } - } - } - - // View Chat - if (context === 'view' || context === 'focused' /* fallback in case 'focused' was not successful */) { - const chatView = await VoiceChatSessionControllerFactory.revealChatView(accessor); - if (chatView) { - return VoiceChatSessionControllerFactory.doCreateForChatView(chatView, viewsService); - } - } - - // Inline Chat - if (context === 'inline') { - const activeCodeEditor = getCodeEditor(editorService.activeTextEditorControl); - if (activeCodeEditor) { - const inlineChat = InlineChatController.get(activeCodeEditor); - if (inlineChat) { - return VoiceChatSessionControllerFactory.doCreateForInlineChat(inlineChat); - } - } - } - - // Terminal Chat - if (context === 'terminal') { - const activeInstance = terminalService.activeInstance; - if (activeInstance) { - const terminalChat = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); - if (terminalChat) { - return VoiceChatSessionControllerFactory.doCreateForTerminalChat(terminalChat); - } - } - } - - // Quick Chat - if (context === 'quick') { - quickChatService.open(); - - const quickChat = chatWidgetService.lastFocusedWidget; - if (quickChat) { - return VoiceChatSessionControllerFactory.doCreateForQuickChat(quickChat, quickChatService); - } - } - - return undefined; - } - - static async revealChatView(accessor: ServicesAccessor): Promise { - const chatService = accessor.get(IChatService); const viewsService = accessor.get(IViewsService); - if (chatService.isEnabled(ChatAgentLocation.Panel)) { - return showChatView(viewsService); + + switch (context) { + case 'focused': { + const controller = VoiceChatSessionControllerFactory.doCreateForFocusedChat(terminalService, chatWidgetService, layoutService); + return controller ?? VoiceChatSessionControllerFactory.create(accessor, 'view'); // fallback to 'view' + } + case 'view': { + const chatWidget = await showChatView(viewsService); + if (chatWidget) { + return VoiceChatSessionControllerFactory.doCreateForChatWidget('view', chatWidget); + } + break; + } + case 'inline': { + const activeCodeEditor = getCodeEditor(editorService.activeTextEditorControl); + if (activeCodeEditor) { + const inlineChat = InlineChatController.get(activeCodeEditor); + if (inlineChat) { + if (!inlineChat.joinCurrentRun()) { + inlineChat.run(); + } + return VoiceChatSessionControllerFactory.doCreateForChatWidget('inline', inlineChat.chatWidget); + } + } + break; + } + case 'quick': { + quickChatService.open(); // this will populate focused chat widget in the chat widget service + return VoiceChatSessionControllerFactory.create(accessor, 'focused'); + } } return undefined; } - private static doCreateForChatView(chatView: IChatWidget, viewsService: IViewsService): IVoiceChatSessionController { - return VoiceChatSessionControllerFactory.doCreateForChatViewOrEditor('view', chatView, viewsService); + private static doCreateForFocusedChat(terminalService: ITerminalService, chatWidgetService: IChatWidgetService, layoutService: IWorkbenchLayoutService): IVoiceChatSessionController | undefined { + + // 1.) probe terminal chat which is not part of chat widget service + const activeInstance = terminalService.activeInstance; + if (activeInstance) { + const terminalChat = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + if (terminalChat?.hasFocus()) { + return VoiceChatSessionControllerFactory.doCreateForTerminalChat(terminalChat); + } + } + + // 2.) otherwise go via chat widget service + const chatWidget = chatWidgetService.lastFocusedWidget; + if (chatWidget?.hasInputFocus()) { + + // Figure out the context of the chat widget by asking + // layout service for the part that has focus. Unfortunately + // there is no better way because the widget does not know + // its location. + + let context: VoiceChatSessionContext; + if (layoutService.hasFocus(Parts.EDITOR_PART)) { + context = chatWidget.location === ChatAgentLocation.Panel ? 'editor' : 'inline'; + } else if ( + [Parts.SIDEBAR_PART, Parts.PANEL_PART, Parts.AUXILIARYBAR_PART, Parts.TITLEBAR_PART, Parts.STATUSBAR_PART, Parts.BANNER_PART, Parts.ACTIVITYBAR_PART].some(part => layoutService.hasFocus(part)) + ) { + context = 'view'; + } else { + context = 'quick'; + } + + return VoiceChatSessionControllerFactory.doCreateForChatWidget(context, chatWidget); + } + + return undefined; } - private static doCreateForChatEditor(chatView: IChatWidget, viewsService: IViewsService): IVoiceChatSessionController { - return VoiceChatSessionControllerFactory.doCreateForChatViewOrEditor('editor', chatView, viewsService); - } - - private static createContextKeyController(contextKeyService: IContextKeyService, rawControllerVoiceChatInProgress: RawContextKey): (state: VoiceChatSessionState) => void { - const contextVoiceChatGettingReady = CONTEXT_VOICE_CHAT_GETTING_READY.bindTo(contextKeyService); - const contextVoiceChatInProgress = CONTEXT_VOICE_CHAT_IN_PROGRESS.bindTo(contextKeyService); - const controllerVoiceChatInProgress = rawControllerVoiceChatInProgress.bindTo(contextKeyService); + private static createContextKeyController(contextKeyService: IContextKeyService, context: VoiceChatSessionContext): (state: VoiceChatSessionState) => void { + const contextVoiceChatGettingReady = SCOPED_VOICE_CHAT_GETTING_READY.bindTo(contextKeyService); + const contextVoiceChatInProgress = SCOPED_VOICE_CHAT_IN_PROGRESS.bindTo(contextKeyService); return (state: VoiceChatSessionState) => { switch (state) { case VoiceChatSessionState.GettingReady: contextVoiceChatGettingReady.set(true); - contextVoiceChatInProgress.set(false); - controllerVoiceChatInProgress.set(false); + contextVoiceChatInProgress.set(undefined); break; case VoiceChatSessionState.Started: contextVoiceChatGettingReady.set(false); - contextVoiceChatInProgress.set(true); - controllerVoiceChatInProgress.set(true); + contextVoiceChatInProgress.set(context); break; case VoiceChatSessionState.Stopped: contextVoiceChatGettingReady.set(false); - contextVoiceChatInProgress.set(false); - controllerVoiceChatInProgress.set(false); + contextVoiceChatInProgress.set(undefined); break; } }; } - private static doCreateForChatViewOrEditor(context: 'view' | 'editor', chatView: IChatWidget, viewsService: IViewsService): IVoiceChatSessionController { + private static doCreateForChatWidget(context: VoiceChatSessionContext, chatWidget: IChatWidget): IVoiceChatSessionController { return { context, - onDidAcceptInput: chatView.onDidAcceptInput, - // TODO@bpasero cancellation needs to work better for chat editors that are not view bound - onDidCancelInput: Event.filter(viewsService.onDidChangeViewVisibility, e => e.id === CHAT_VIEW_ID), - focusInput: () => chatView.focusInput(), - acceptInput: () => chatView.acceptInput(), - updateInput: text => chatView.setInput(text), - getInput: () => chatView.getInput(), - setInputPlaceholder: text => chatView.setInputPlaceholder(text), - clearInputPlaceholder: () => chatView.resetInputPlaceholder(), - updateState: VoiceChatSessionControllerFactory.createContextKeyController(chatView.scopedContextKeyService, CONTEXT_VOICE_CHAT_IN_VIEW_IN_PROGRESS) - }; - } - - private static doCreateForQuickChat(quickChat: IChatWidget, quickChatService: IQuickChatService): IVoiceChatSessionController { - return { - context: 'quick', - onDidAcceptInput: quickChat.onDidAcceptInput, - onDidCancelInput: quickChatService.onDidClose, - focusInput: () => quickChat.focusInput(), - acceptInput: () => quickChat.acceptInput(), - updateInput: text => quickChat.setInput(text), - getInput: () => quickChat.getInput(), - setInputPlaceholder: text => quickChat.setInputPlaceholder(text), - clearInputPlaceholder: () => quickChat.resetInputPlaceholder(), - updateState: VoiceChatSessionControllerFactory.createContextKeyController(quickChat.scopedContextKeyService, CONTEXT_QUICK_VOICE_CHAT_IN_PROGRESS) - }; - } - - private static doCreateForInlineChat(inlineChat: InlineChatController): IVoiceChatSessionController { - const inlineChatSession = inlineChat.joinCurrentRun() ?? inlineChat.run(); - - return { - context: 'inline', - onDidAcceptInput: inlineChat.onDidAcceptInput, - onDidCancelInput: Event.any( - inlineChat.onDidCancelInput, - Event.fromPromise(inlineChatSession) - ), - focusInput: () => inlineChat.focus(), - acceptInput: () => inlineChat.acceptInput(), - updateInput: text => inlineChat.updateInput(text, false), - getInput: () => inlineChat.getInput(), - setInputPlaceholder: text => inlineChat.setPlaceholder(text), - clearInputPlaceholder: () => inlineChat.resetPlaceholder(), - updateState: VoiceChatSessionControllerFactory.createContextKeyController(inlineChat.scopedContextKeyService, CONTEXT_INLINE_VOICE_CHAT_IN_PROGRESS) + onDidAcceptInput: chatWidget.onDidAcceptInput, + onDidHideInput: chatWidget.onDidHideInput, + focusInput: () => chatWidget.focusInput(), + acceptInput: () => chatWidget.acceptInput(), + updateInput: text => chatWidget.setInput(text), + getInput: () => chatWidget.getInput(), + setInputPlaceholder: text => chatWidget.setInputPlaceholder(text), + clearInputPlaceholder: () => chatWidget.resetInputPlaceholder(), + updateState: VoiceChatSessionControllerFactory.createContextKeyController(chatWidget.scopedContextKeyService, context) }; } private static doCreateForTerminalChat(terminalChat: TerminalChatController): IVoiceChatSessionController { + const context = 'terminal'; return { - context: 'terminal', + context, onDidAcceptInput: terminalChat.onDidAcceptInput, - onDidCancelInput: terminalChat.onDidCancelInput, + onDidHideInput: terminalChat.onDidHideInput, focusInput: () => terminalChat.focus(), acceptInput: () => terminalChat.acceptInput(), updateInput: text => terminalChat.updateInput(text, false), getInput: () => terminalChat.getInput(), setInputPlaceholder: text => terminalChat.setPlaceholder(text), clearInputPlaceholder: () => terminalChat.resetPlaceholder(), - updateState: VoiceChatSessionControllerFactory.createContextKeyController(terminalChat.scopedContextKeyService, CONTEXT_VOICE_CHAT_IN_TERMINAL_IN_PROGRESS) + updateState: VoiceChatSessionControllerFactory.createContextKeyController(terminalChat.scopedContextKeyService, context) }; } } @@ -369,7 +288,7 @@ class VoiceChatSessions { session.disposables.add(toDisposable(() => cts.dispose(true))); session.disposables.add(controller.onDidAcceptInput(() => this.stop(sessionId, controller.context))); - session.disposables.add(controller.onDidCancelInput(() => this.stop(sessionId, controller.context))); + session.disposables.add(controller.onDidHideInput(() => this.stop(sessionId, controller.context))); controller.focusInput(); @@ -476,7 +395,7 @@ class VoiceChatSessions { export const VOICE_KEY_HOLD_THRESHOLD = 500; -async function startVoiceChatWithHoldMode(id: string, accessor: ServicesAccessor, target: 'inline' | 'quick' | 'view' | 'focused', context?: IChatExecuteActionContext): Promise { +async function startVoiceChatWithHoldMode(id: string, accessor: ServicesAccessor, target: 'view' | 'inline' | 'quick' | 'focused', context?: IChatExecuteActionContext): Promise { const instantiationService = accessor.get(IInstantiationService); const keybindingService = accessor.get(IKeybindingService); @@ -504,7 +423,7 @@ async function startVoiceChatWithHoldMode(id: string, accessor: ServicesAccessor class VoiceChatWithHoldModeAction extends Action2 { - constructor(desc: Readonly, private readonly target: 'inline' | 'quick' | 'view') { + constructor(desc: Readonly, private readonly target: 'view' | 'inline' | 'quick') { super(desc); } @@ -561,6 +480,7 @@ export class HoldToVoiceChatInChatViewAction extends Action2 { const instantiationService = accessor.get(IInstantiationService); const keybindingService = accessor.get(IKeybindingService); + const viewsService = accessor.get(IViewsService); const holdMode = keybindingService.enableKeybindingHoldMode(HoldToVoiceChatInChatViewAction.ID); @@ -573,7 +493,7 @@ export class HoldToVoiceChatInChatViewAction extends Action2 { } }, VOICE_KEY_HOLD_THRESHOLD); - (await VoiceChatSessionControllerFactory.revealChatView(accessor))?.focusInput(); + (await showChatView(viewsService))?.focusInput(); await holdMode; handle.dispose(); @@ -634,36 +554,34 @@ export class StartVoiceChatAction extends Action2 { keybinding: { weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and( - FocusInChatInput, // scope this action to chat input fields only - EditorContextKeys.focus.negate(), // do not steal the editor inline-chat keybinding - NOTEBOOK_EDITOR_FOCUSED.negate() // do not steal the notebook inline-chat keybinding + FocusInChatInput, // scope this action to chat input fields only + EditorContextKeys.focus.negate(), // do not steal the editor inline-chat keybinding + NOTEBOOK_EDITOR_FOCUSED.negate() // do not steal the notebook inline-chat keybinding ), primary: KeyMod.CtrlCmd | KeyCode.KeyI }, icon: Codicon.mic, precondition: ContextKeyExpr.and( CanVoiceChat, - CONTEXT_VOICE_CHAT_GETTING_READY.negate(), // disable when voice chat is getting ready - AnyChatRequestInProgress?.negate(), // disable when any chat request is in progress - SpeechToTextInProgress.negate() // disable when speech to text is in progress + SCOPED_VOICE_CHAT_GETTING_READY.negate(), // disable when voice chat is getting ready + AnyChatRequestInProgress?.negate(), // disable when any chat request is in progress + SpeechToTextInProgress.negate() // disable when speech to text is in progress ), menu: [{ id: MenuId.ChatExecute, when: ContextKeyExpr.and( HasSpeechProvider, - TextToSpeechInProgress.negate(), // hide when text to speech is in progress - CONTEXT_VOICE_CHAT_IN_VIEW_IN_PROGRESS.negate(), // hide when voice chat is in progress - CONTEXT_QUICK_VOICE_CHAT_IN_PROGRESS.negate(), // || - CONTEXT_VOICE_CHAT_IN_EDITOR_IN_PROGRESS.negate(), // || + TextToSpeechInProgress.negate(), // hide when text to speech is in progress + ScopedVoiceChatInProgress?.negate(), // hide when voice chat is in progress ), group: 'navigation', order: -1 }, { - id: MenuId.for('terminalChatInput'), + id: TerminalChatExecute, when: ContextKeyExpr.and( HasSpeechProvider, - TextToSpeechInProgress.negate(), // hide when text to speech is in progress - CONTEXT_VOICE_CHAT_IN_TERMINAL_IN_PROGRESS.negate(), // hide when voice chat is in progress + TextToSpeechInProgress.negate(), // hide when text to speech is in progress + ScopedVoiceChatInProgress?.negate(), // hide when voice chat is in progress ), group: 'navigation', order: -1 @@ -678,9 +596,6 @@ export class StartVoiceChatAction extends Action2 { // from a toolbar within the chat widget, then make sure // to move focus into the input field so that the controller // is properly retrieved - // TODO@bpasero this will actually not work if the button - // is clicked from the inline editor while focus is in a - // chat input field in a view or picker widget.focusInput(); } @@ -688,133 +603,41 @@ export class StartVoiceChatAction extends Action2 { } } -const InstallingSpeechProvider = new RawContextKey('installingSpeechProvider', false, true); +export class StopListeningAction extends Action2 { -abstract class BaseInstallSpeechProviderAction extends Action2 { - - private static readonly SPEECH_EXTENSION_ID = 'ms-vscode.vscode-speech'; - - async run(accessor: ServicesAccessor): Promise { - const contextKeyService = accessor.get(IContextKeyService); - const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService); - try { - InstallingSpeechProvider.bindTo(contextKeyService).set(true); - await extensionsWorkbenchService.install(BaseInstallSpeechProviderAction.SPEECH_EXTENSION_ID, { - justification: this.getJustification(), - enable: true - }, ProgressLocation.Notification); - } finally { - InstallingSpeechProvider.bindTo(contextKeyService).set(false); - } - } - - protected abstract getJustification(): string; -} - -export class InstallSpeechProviderForVoiceChatAction extends BaseInstallSpeechProviderAction { - - static readonly ID = 'workbench.action.chat.installProviderForVoiceChat'; + static readonly ID = 'workbench.action.chat.stopListening'; constructor() { super({ - id: InstallSpeechProviderForVoiceChatAction.ID, - title: localize2('workbench.action.chat.installProviderForVoiceChat.label', "Start Voice Chat"), - icon: Codicon.mic, - precondition: InstallingSpeechProvider.negate(), + id: StopListeningAction.ID, + title: localize2('workbench.action.chat.stopListening.label', "Stop Listening"), + category: CHAT_CATEGORY, + f1: true, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib + 100, + primary: KeyCode.Escape + }, + icon: spinningLoading, + precondition: GlobalVoiceChatInProgress, // need global context here because of `f1: true` menu: [{ id: MenuId.ChatExecute, - when: HasSpeechProvider.negate(), + when: ScopedVoiceChatInProgress, group: 'navigation', order: -1 }, { - id: MenuId.for('terminalChatInput'), - when: HasSpeechProvider.negate(), + id: TerminalChatExecute, + when: ScopedVoiceChatInProgress, group: 'navigation', order: -1 }] }); } - protected getJustification(): string { - return localize('installProviderForVoiceChat.justification', "Microphone support requires this extension."); - } -} - -class BaseStopListeningAction extends Action2 { - - constructor( - desc: { id: string; icon?: ThemeIcon; f1?: boolean }, - context: RawContextKey, - menu: MenuId | undefined, - ) { - super({ - ...desc, - title: localize2('workbench.action.chat.stopListening.label', "Stop Listening"), - category: CHAT_CATEGORY, - keybinding: { - weight: KeybindingWeight.WorkbenchContrib + 100, - primary: KeyCode.Escape - }, - precondition: ContextKeyExpr.and(CanVoiceChat, context), - menu: menu ? [{ - id: menu, - when: ContextKeyExpr.and(CanVoiceChat, context), - group: 'navigation', - order: -1 - }] : undefined - }); - } - async run(accessor: ServicesAccessor): Promise { VoiceChatSessions.getInstance(accessor.get(IInstantiationService)).stop(); } } -export class StopListeningAction extends BaseStopListeningAction { - - static readonly ID = 'workbench.action.chat.stopListening'; - - constructor() { - super({ id: StopListeningAction.ID, f1: true }, CONTEXT_VOICE_CHAT_IN_PROGRESS, undefined); - } -} - -export class StopListeningInChatViewAction extends BaseStopListeningAction { - - static readonly ID = 'workbench.action.chat.stopListeningInChatView'; - - constructor() { - super({ id: StopListeningInChatViewAction.ID, icon: spinningLoading }, CONTEXT_VOICE_CHAT_IN_VIEW_IN_PROGRESS, MenuId.ChatExecute); - } -} - -export class StopListeningInChatEditorAction extends BaseStopListeningAction { - - static readonly ID = 'workbench.action.chat.stopListeningInChatEditor'; - - constructor() { - super({ id: StopListeningInChatEditorAction.ID, icon: spinningLoading }, CONTEXT_VOICE_CHAT_IN_EDITOR_IN_PROGRESS, MenuId.ChatExecute); - } -} - -export class StopListeningInQuickChatAction extends BaseStopListeningAction { - - static readonly ID = 'workbench.action.chat.stopListeningInQuickChat'; - - constructor() { - super({ id: StopListeningInQuickChatAction.ID, icon: spinningLoading }, CONTEXT_QUICK_VOICE_CHAT_IN_PROGRESS, MenuId.ChatExecute); - } -} - -export class StopListeningInTerminalChatAction extends BaseStopListeningAction { - - static readonly ID = 'workbench.action.chat.stopListeningInTerminalChat'; - - constructor() { - super({ id: StopListeningInTerminalChatAction.ID, icon: spinningLoading }, CONTEXT_VOICE_CHAT_IN_TERMINAL_IN_PROGRESS, MenuId.for('terminalChatInput')); - } -} - export class StopListeningAndSubmitAction extends Action2 { static readonly ID = 'workbench.action.chat.stopListeningAndSubmit'; @@ -830,7 +653,7 @@ export class StopListeningAndSubmitAction extends Action2 { when: FocusInChatInput, primary: KeyMod.CtrlCmd | KeyCode.KeyI }, - precondition: ContextKeyExpr.and(CanVoiceChat, CONTEXT_VOICE_CHAT_IN_PROGRESS) + precondition: GlobalVoiceChatInProgress // need global context here because of `f1: true` }); } @@ -941,35 +764,11 @@ class ChatSynthesizerSessions { } } -export class InstallSpeechProviderForSynthesizeChatAction extends BaseInstallSpeechProviderAction { - - static readonly ID = 'workbench.action.chat.installProviderForSynthesis'; - - constructor() { - super({ - id: InstallSpeechProviderForSynthesizeChatAction.ID, - title: localize2('workbench.action.chat.installProviderForSynthesis.label', "Read Aloud"), - icon: Codicon.unmute, - precondition: InstallingSpeechProvider.negate(), - menu: [{ - id: MenuId.ChatMessageTitle, - when: HasSpeechProvider.negate(), - group: 'navigation' - }] - }); - } - - protected getJustification(): string { - return localize('installProviderForSynthesis.justification', "Speaker support requires this extension."); - } -} - export class ReadChatResponseAloud extends Action2 { constructor() { super({ id: 'workbench.action.chat.readChatResponseAloud', title: localize2('workbench.action.chat.readChatResponseAloud', "Read Aloud"), - f1: false, icon: Codicon.unmute, precondition: CanVoiceChat, menu: { @@ -1019,7 +818,7 @@ export class StopReadAloud extends Action2 { order: -1 }, { - id: MenuId.for('terminalChatInput'), + id: TerminalChatExecute, when: TextToSpeechInProgress, group: 'navigation', order: -1 @@ -1312,6 +1111,85 @@ class KeywordActivationStatusEntry extends Disposable { //#endregion +//#region Install Provider Actions + +const InstallingSpeechProvider = new RawContextKey('installingSpeechProvider', false, true); + +abstract class BaseInstallSpeechProviderAction extends Action2 { + + private static readonly SPEECH_EXTENSION_ID = 'ms-vscode.vscode-speech'; + + async run(accessor: ServicesAccessor): Promise { + const contextKeyService = accessor.get(IContextKeyService); + const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService); + try { + InstallingSpeechProvider.bindTo(contextKeyService).set(true); + await extensionsWorkbenchService.install(BaseInstallSpeechProviderAction.SPEECH_EXTENSION_ID, { + justification: this.getJustification(), + enable: true + }, ProgressLocation.Notification); + } finally { + InstallingSpeechProvider.bindTo(contextKeyService).set(false); + } + } + + protected abstract getJustification(): string; +} + +export class InstallSpeechProviderForVoiceChatAction extends BaseInstallSpeechProviderAction { + + static readonly ID = 'workbench.action.chat.installProviderForVoiceChat'; + + constructor() { + super({ + id: InstallSpeechProviderForVoiceChatAction.ID, + title: localize2('workbench.action.chat.installProviderForVoiceChat.label', "Start Voice Chat"), + icon: Codicon.mic, + precondition: InstallingSpeechProvider.negate(), + menu: [{ + id: MenuId.ChatExecute, + when: HasSpeechProvider.negate(), + group: 'navigation', + order: -1 + }, { + id: TerminalChatExecute, + when: HasSpeechProvider.negate(), + group: 'navigation', + order: -1 + }] + }); + } + + protected getJustification(): string { + return localize('installProviderForVoiceChat.justification', "Microphone support requires this extension."); + } +} + +export class InstallSpeechProviderForSynthesizeChatAction extends BaseInstallSpeechProviderAction { + + static readonly ID = 'workbench.action.chat.installProviderForSynthesis'; + + constructor() { + super({ + id: InstallSpeechProviderForSynthesizeChatAction.ID, + title: localize2('workbench.action.chat.installProviderForSynthesis.label', "Read Aloud"), + icon: Codicon.unmute, + precondition: InstallingSpeechProvider.negate(), + menu: [{ + id: MenuId.ChatMessageTitle, + when: HasSpeechProvider.negate(), + group: 'navigation' + }] + }); + } + + protected getJustification(): string { + return localize('installProviderForSynthesis.justification', "Speaker support requires this extension."); + } +} + +//#endregion + registerThemingParticipant((theme, collector) => { let activeRecordingColor: Color | undefined; let activeRecordingDimmedColor: Color | undefined; diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts b/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts index 443aafd4150..9fca2cd9497 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { InlineVoiceChatAction, QuickVoiceChatAction, StartVoiceChatAction, StopListeningInQuickChatAction, StopListeningInChatEditorAction, StopListeningInChatViewAction, VoiceChatInChatViewAction, StopListeningAction, StopListeningAndSubmitAction, KeywordActivationContribution, InstallSpeechProviderForSynthesizeChatAction, InstallSpeechProviderForVoiceChatAction, StopListeningInTerminalChatAction, HoldToVoiceChatInChatViewAction, ReadChatResponseAloud, StopReadAloud, StopReadChatItemAloud } from 'vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions'; +import { InlineVoiceChatAction, QuickVoiceChatAction, StartVoiceChatAction, VoiceChatInChatViewAction, StopListeningAction, StopListeningAndSubmitAction, KeywordActivationContribution, InstallSpeechProviderForSynthesizeChatAction, InstallSpeechProviderForVoiceChatAction, HoldToVoiceChatInChatViewAction, ReadChatResponseAloud, StopReadAloud, StopReadChatItemAloud } from 'vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions'; import { registerAction2 } from 'vs/platform/actions/common/actions'; import { WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; @@ -18,11 +18,6 @@ registerAction2(InlineVoiceChatAction); registerAction2(StopListeningAction); registerAction2(StopListeningAndSubmitAction); -registerAction2(StopListeningInChatViewAction); -registerAction2(StopListeningInChatEditorAction); -registerAction2(StopListeningInQuickChatAction); -registerAction2(StopListeningInTerminalChatAction); - registerAction2(ReadChatResponseAloud); registerAction2(StopReadChatItemAloud); registerAction2(StopReadAloud); diff --git a/src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts b/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts similarity index 98% rename from src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts rename to src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts index 74ef88eca5f..2a2c9447a01 100644 --- a/src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts @@ -11,10 +11,11 @@ import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifec import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ProviderResult } from 'vs/editor/common/languages'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { ChatAgentLocation, IChatAgent, IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentImplementation, IChatAgentMetadata, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChatProgress, IChatFollowup } from 'vs/workbench/contrib/chat/common/chatService'; -import { IVoiceChatSessionOptions, IVoiceChatTextEvent, VoiceChatService } from 'vs/workbench/contrib/chat/common/voiceChat'; +import { IVoiceChatSessionOptions, IVoiceChatTextEvent, VoiceChatService } from 'vs/workbench/contrib/chat/common/voiceChatService'; import { ISpeechProvider, ISpeechService, ISpeechToTextEvent, ISpeechToTextSession, ITextToSpeechSession, KeywordRecognitionStatus, SpeechToTextStatus } from 'vs/workbench/contrib/speech/common/speechService'; import { nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; @@ -121,7 +122,7 @@ suite('VoiceChat', () => { setup(() => { emitter = disposables.add(new Emitter()); - service = disposables.add(new VoiceChatService(new TestSpeechService(), new TestChatAgentService())); + service = disposables.add(new VoiceChatService(new TestSpeechService(), new TestChatAgentService(), new MockContextKeyService())); }); teardown(() => { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 1eb3cd093c1..007eab054be 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -122,8 +122,13 @@ export class InlineChatController implements IEditorContribution { private readonly _onWillStartSession = this._store.add(new Emitter()); readonly onWillStartSession = this._onWillStartSession.event; - readonly onDidAcceptInput = Event.filter(this._messages.event, m => m === Message.ACCEPT_INPUT, this._store); - readonly onDidCancelInput = Event.filter(this._messages.event, m => m === Message.CANCEL_INPUT || m === Message.CANCEL_SESSION, this._store); + get chatWidget() { + if (this._input.value.isVisible) { + return this._input.value.chatWidget; + } else { + return this._zone.value.widget.chatWidget; + } + } private readonly _sessionStore = this._store.add(new DisposableStore()); private readonly _stashedSession = this._store.add(new MutableDisposable()); @@ -1019,35 +1024,13 @@ export class InlineChatController implements IEditorContribution { // ---- controller API - get scopedContextKeyService(): IContextKeyService { - if (this._input.value.isVisible) { - return this._input.value.chatWidget.scopedContextKeyService; - } else { - return this._zone.value.widget.chatWidget.scopedContextKeyService; - } - } - showSaveHint(): void { const status = localize('savehint', "Accept or discard changes to continue saving"); this._zone.value.widget.updateStatus(status, { classes: ['warn'] }); } - setPlaceholder(text: string): void { - this._forcedPlaceholder = text; - this._updatePlaceholder(); - } - - resetPlaceholder(): void { - this._forcedPlaceholder = undefined; - this._updatePlaceholder(); - } - acceptInput() { - if (this._input.value.isVisible) { - return this._input.value.chatWidget.acceptInput(); - } else { - return this._zone.value.widget.chatWidget.acceptInput(); - } + return this.chatWidget.acceptInput(); } updateInput(text: string, selectAll = true): void { @@ -1061,12 +1044,6 @@ export class InlineChatController implements IEditorContribution { } } - getInput(): string { - return this._input.value.isVisible - ? this._input.value.value - : this._zone.value.widget.value; - } - cancelCurrentRequest(): void { this._messages.fire(Message.CANCEL_INPUT | Message.CANCEL_REQUEST); } diff --git a/src/vs/workbench/contrib/speech/common/speechService.ts b/src/vs/workbench/contrib/speech/common/speechService.ts index 4d39702b1e2..b915e7d394f 100644 --- a/src/vs/workbench/contrib/speech/common/speechService.ts +++ b/src/vs/workbench/contrib/speech/common/speechService.ts @@ -14,9 +14,9 @@ import { language } from 'vs/base/common/platform'; export const ISpeechService = createDecorator('speechService'); -export const HasSpeechProvider = new RawContextKey('hasSpeechProvider', false, { type: 'string', description: localize('hasSpeechProvider', "A speech provider is registered to the speech service.") }); -export const SpeechToTextInProgress = new RawContextKey('speechToTextInProgress', false, { type: 'string', description: localize('speechToTextInProgress', "A speech-to-text session is in progress.") }); -export const TextToSpeechInProgress = new RawContextKey('textToSpeechInProgress', false, { type: 'string', description: localize('textToSpeechInProgress', "A text-to-speech session is in progress.") }); +export const HasSpeechProvider = new RawContextKey('hasSpeechProvider', false, { type: 'boolean', description: localize('hasSpeechProvider', "A speech provider is registered to the speech service.") }); +export const SpeechToTextInProgress = new RawContextKey('speechToTextInProgress', false, { type: 'boolean', description: localize('speechToTextInProgress', "A speech-to-text session is in progress.") }); +export const TextToSpeechInProgress = new RawContextKey('textToSpeechInProgress', false, { type: 'boolean', description: localize('textToSpeechInProgress', "A text-to-speech session is in progress.") }); export interface ISpeechProviderMetadata { readonly extension: ExtensionIdentifier; diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index a965feab2b4..de7e10f9916 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -78,7 +78,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr } readonly onDidAcceptInput = Event.filter(this._messages.event, m => m === Message.ACCEPT_INPUT, this._store); - readonly onDidCancelInput = Event.filter(this._messages.event, m => m === Message.CANCEL_INPUT || m === Message.CANCEL_SESSION, this._store); + get onDidHideInput() { return this.chatWidget?.onDidHideInput ?? Event.None; } private _terminalAgentName = 'terminal'; private _terminalAgentId: string | undefined; diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 841c856e64f..2c337e076f9 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -5,7 +5,7 @@ import type { Terminal as RawXtermTerminal } from '@xterm/xterm'; import { Dimension, getActiveWindow, IFocusTracker, trackFocus } from 'vs/base/browser/dom'; -import { Event } from 'vs/base/common/event'; +import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { MicrotaskDelay } from 'vs/base/common/symbols'; import 'vs/css!./media/terminalChatWidget'; @@ -27,6 +27,9 @@ export class TerminalChatWidget extends Disposable { private readonly _container: HTMLElement; + private readonly _onDidHideInput = this._register(new Emitter()); + readonly onDidHideInput = this._onDidHideInput.event; + private readonly _inlineChatWidget: InlineChatWidget; public get inlineChatWidget(): InlineChatWidget { return this._inlineChatWidget; } @@ -171,6 +174,7 @@ export class TerminalChatWidget extends Disposable { this._inlineChatWidget.value = ''; this._instance.focus(); this._setTerminalOffset(undefined); + this._onDidHideInput.fire(); } private _setTerminalOffset(offset: number | undefined) { if (offset === undefined || this._container.classList.contains('hide')) { From 5dd248fc121c8617c1637d4f2cd8fbc1e0184bae Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Tue, 14 May 2024 10:52:51 +0200 Subject: [PATCH 160/357] Fixes #212293 (#212675) --- .../contrib/codeEditor/browser/diffEditorHelper.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts b/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts index 27448b3a815..052608edd82 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts @@ -9,9 +9,9 @@ import { IDiffEditor } from 'vs/editor/browser/editorBrowser'; import { registerDiffEditorContribution } from 'vs/editor/browser/editorExtensions'; import { EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/embeddedDiffEditorWidget'; import { IDiffEditorContribution } from 'vs/editor/common/editorCommon'; +import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; import { localize } from 'vs/nls'; import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -25,7 +25,7 @@ class DiffEditorHelperContribution extends Disposable implements IDiffEditorCont constructor( private readonly _diffEditor: IDiffEditor, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IConfigurationService private readonly _configurationService: IConfigurationService, + @ITextResourceConfigurationService private readonly _textResourceConfigurationService: ITextResourceConfigurationService, @INotificationService private readonly _notificationService: INotificationService, ) { super(); @@ -46,7 +46,7 @@ class DiffEditorHelperContribution extends Disposable implements IDiffEditorCont null )); store.add(helperWidget.onClick(() => { - this._configurationService.updateValue('diffEditor.ignoreTrimWhitespace', false); + this._textResourceConfigurationService.updateValue(this._diffEditor.getModel()!.modified.uri, 'diffEditor.ignoreTrimWhitespace', false); })); helperWidget.render(); } @@ -62,7 +62,7 @@ class DiffEditorHelperContribution extends Disposable implements IDiffEditorCont [{ label: localize('removeTimeout', "Remove Limit"), run: () => { - this._configurationService.updateValue('diffEditor.maxComputationTime', 0); + this._textResourceConfigurationService.updateValue(this._diffEditor.getModel()!.modified.uri, 'diffEditor.maxComputationTime', 0); } }], {} From b2e6cd0212dd5751bc852591608036fa6d76adbd Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Tue, 14 May 2024 11:28:12 +0200 Subject: [PATCH 161/357] Adresses #211878 (#212677) --- .../workbench/contrib/mergeEditor/browser/commands/commands.ts | 2 +- src/vs/workbench/contrib/mergeEditor/browser/utils.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/mergeEditor/browser/commands/commands.ts b/src/vs/workbench/contrib/mergeEditor/browser/commands/commands.ts index 39659a12926..5b6205106fe 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/commands/commands.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/commands/commands.ts @@ -487,7 +487,7 @@ async function mergeEditorCompare(viewModel: MergeEditorViewModel, editorService }, revealIfOpened: true, revealIfVisible: true, - } as ITextEditorOptions + } satisfies ITextEditorOptions }); } diff --git a/src/vs/workbench/contrib/mergeEditor/browser/utils.ts b/src/vs/workbench/contrib/mergeEditor/browser/utils.ts index 5ac6522a14b..abc62bf6b7e 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/utils.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/utils.ts @@ -103,7 +103,7 @@ export function setFields(obj: T, fields: Partial): T { } export function deepMerge(source1: T, source2: Partial): T { - const result = {} as T; + const result = {} as any as T; for (const key in source1) { result[key] = source1[key]; } From bdddba5a82ec5aa781a256aeee29240c09bda880 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 14 May 2024 15:15:38 +0200 Subject: [PATCH 162/357] fix #212105 (#212693) --- src/vs/server/node/remoteExtensionsScanner.ts | 43 +++++++++++++++---- .../common/extensionManagement.ts | 1 + .../common/extensionManagementService.ts | 8 +++- .../cachedExtensionScanner.ts | 4 +- .../remote/common/remoteExtensionsScanner.ts | 12 +++++- .../test/browser/workbenchTestServices.ts | 1 + 6 files changed, 56 insertions(+), 13 deletions(-) diff --git a/src/vs/server/node/remoteExtensionsScanner.ts b/src/vs/server/node/remoteExtensionsScanner.ts index 95e9c62afc8..54418aebfc2 100644 --- a/src/vs/server/node/remoteExtensionsScanner.ts +++ b/src/vs/server/node/remoteExtensionsScanner.ts @@ -79,7 +79,13 @@ export class RemoteExtensionsScannerService implements IRemoteExtensionsScannerS return this._whenExtensionsReady; } - async scanExtensions(language?: string, profileLocation?: URI, extensionDevelopmentLocations?: URI[], languagePackId?: string): Promise { + async scanExtensions( + language?: string, + profileLocation?: URI, + workspaceExtensionLocations?: URI[], + extensionDevelopmentLocations?: URI[], + languagePackId?: string + ): Promise { performance.mark('code/server/willScanExtensions'); this._logService.trace(`Scanning extensions using UI language: ${language}`); @@ -88,7 +94,7 @@ export class RemoteExtensionsScannerService implements IRemoteExtensionsScannerS const extensionDevelopmentPaths = extensionDevelopmentLocations ? extensionDevelopmentLocations.filter(url => url.scheme === Schemas.file).map(url => url.fsPath) : undefined; profileLocation = profileLocation ?? this._userDataProfilesService.defaultProfile.extensionsResource; - const extensions = await this._scanExtensions(profileLocation, language ?? platform.language, extensionDevelopmentPaths, languagePackId); + const extensions = await this._scanExtensions(profileLocation, language ?? platform.language, workspaceExtensionLocations, extensionDevelopmentPaths, languagePackId); this._logService.trace('Scanned Extensions', extensions); this._massageWhenConditions(extensions); @@ -117,16 +123,17 @@ export class RemoteExtensionsScannerService implements IRemoteExtensionsScannerS return extension; } - private async _scanExtensions(profileLocation: URI, language: string, extensionDevelopmentPath: string[] | undefined, languagePackId: string | undefined): Promise { + private async _scanExtensions(profileLocation: URI, language: string, workspaceInstalledExtensionLocations: URI[] | undefined, extensionDevelopmentPath: string[] | undefined, languagePackId: string | undefined): Promise { await this._ensureLanguagePackIsInstalled(language, languagePackId); - const [builtinExtensions, installedExtensions, developedExtensions] = await Promise.all([ + const [builtinExtensions, installedExtensions, workspaceInstalledExtensions, developedExtensions] = await Promise.all([ this._scanBuiltinExtensions(language), this._scanInstalledExtensions(profileLocation, language), + this._scanWorkspaceInstalledExtensions(language, workspaceInstalledExtensionLocations), this._scanDevelopedExtensions(language, extensionDevelopmentPath) ]); - return dedupExtensions(builtinExtensions, installedExtensions, developedExtensions, this._logService); + return dedupExtensions(builtinExtensions, [...installedExtensions, ...workspaceInstalledExtensions], developedExtensions, this._logService); } private async _scanDevelopedExtensions(language: string, extensionDevelopmentPaths?: string[]): Promise { @@ -138,6 +145,19 @@ export class RemoteExtensionsScannerService implements IRemoteExtensionsScannerS return []; } + private async _scanWorkspaceInstalledExtensions(language: string, workspaceInstalledExtensions?: URI[]): Promise { + const result: IExtensionDescription[] = []; + if (workspaceInstalledExtensions?.length) { + const scannedExtensions = await Promise.all(workspaceInstalledExtensions.map(location => this._extensionsScannerService.scanExistingExtension(location, ExtensionType.User, { language }))); + for (const scannedExtension of scannedExtensions) { + if (scannedExtension) { + result.push(toExtensionDescription(scannedExtension, false)); + } + } + } + return result; + } + private async _scanBuiltinExtensions(language: string): Promise { const scannedExtensions = await this._extensionsScannerService.scanSystemExtensions({ language, useCache: true }); return scannedExtensions.map(e => toExtensionDescription(e, false)); @@ -319,9 +339,16 @@ export class RemoteExtensionsScannerChannel implements IServerChannel { case 'scanExtensions': { const language = args[0]; const profileLocation = args[1] ? URI.revive(uriTransformer.transformIncoming(args[1])) : undefined; - const extensionDevelopmentPath = Array.isArray(args[2]) ? args[2].map(u => URI.revive(uriTransformer.transformIncoming(u))) : undefined; - const languagePackId: string | undefined = args[3]; - const extensions = await this.service.scanExtensions(language, profileLocation, extensionDevelopmentPath, languagePackId); + const workspaceExtensionLocations = Array.isArray(args[2]) ? args[2].map(u => URI.revive(uriTransformer.transformIncoming(u))) : undefined; + const extensionDevelopmentPath = Array.isArray(args[3]) ? args[3].map(u => URI.revive(uriTransformer.transformIncoming(u))) : undefined; + const languagePackId: string | undefined = args[4]; + const extensions = await this.service.scanExtensions( + language, + profileLocation, + workspaceExtensionLocations, + extensionDevelopmentPath, + languagePackId + ); return extensions.map(extension => transformOutgoingURIs(extension, uriTransformer)); } case 'scanSingleExtension': { diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts index 0feee51ce96..3c7751e1f6b 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts @@ -68,6 +68,7 @@ export interface IWorkbenchExtensionManagementService extends IProfileAwareExten onDidEnableExtensions: Event; getExtensions(locations: URI[]): Promise; + getInstalledWorkspaceExtensionLocations(): URI[]; getInstalledWorkspaceExtensions(includeInvalid: boolean): Promise; canInstall(extension: IGalleryExtension | IResourceExtension): Promise; diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index 2bbfd336f4d..7e4bc409689 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -448,6 +448,10 @@ export class ExtensionManagementService extends Disposable implements IWorkbench return result; } + getInstalledWorkspaceExtensionLocations(): URI[] { + return this.workspaceExtensionManagementService.getInstalledWorkspaceExtensionsLocations(); + } + async getInstalledWorkspaceExtensions(includeInvalid: boolean): Promise { return this.workspaceExtensionManagementService.getInstalled(includeInvalid); } @@ -837,7 +841,7 @@ class WorkspaceExtensionsManagementService extends Disposable { } private async initialize(): Promise { - const existingLocations = this.getWorkspaceExtensionsLocations(); + const existingLocations = this.getInstalledWorkspaceExtensionsLocations(); if (!existingLocations.length) { return; } @@ -943,7 +947,7 @@ class WorkspaceExtensionsManagementService extends Disposable { }>('workspaceextension:uninstall'); } - private getWorkspaceExtensionsLocations(): URI[] { + getInstalledWorkspaceExtensionsLocations(): URI[] { const locations: URI[] = []; try { const parsed = JSON.parse(this.storageService.get(WorkspaceExtensionsManagementService.WORKSPACE_EXTENSIONS_KEY, StorageScope.WORKSPACE, '[]')); diff --git a/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts b/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts index 70910c8ddb9..1c2168068b1 100644 --- a/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts +++ b/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts @@ -19,6 +19,7 @@ import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/c import { getErrorMessage } from 'vs/base/common/errors'; import { IWorkbenchExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { toExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; export class CachedExtensionScanner { @@ -32,6 +33,7 @@ export class CachedExtensionScanner { @IExtensionsScannerService private readonly _extensionsScannerService: IExtensionsScannerService, @IUserDataProfileService private readonly _userDataProfileService: IUserDataProfileService, @IWorkbenchExtensionManagementService private readonly _extensionManagementService: IWorkbenchExtensionManagementService, + @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, @ILogService private readonly _logService: ILogService, ) { this.scannedExtensions = new Promise((resolve, reject) => { @@ -60,7 +62,7 @@ export class CachedExtensionScanner { const result = await Promise.allSettled([ this._extensionsScannerService.scanSystemExtensions({ language, useCache: true, checkControlFile: true }), this._extensionsScannerService.scanUserExtensions({ language, profileLocation: this._userDataProfileService.currentProfile.extensionsResource, useCache: true }), - this._extensionManagementService.getInstalledWorkspaceExtensions(false) + this._environmentService.remoteAuthority ? [] : this._extensionManagementService.getInstalledWorkspaceExtensions(false) ]); let scannedSystemExtensions: IScannedExtension[] = [], diff --git a/src/vs/workbench/services/remote/common/remoteExtensionsScanner.ts b/src/vs/workbench/services/remote/common/remoteExtensionsScanner.ts index a466cc1f3a1..89e2791637e 100644 --- a/src/vs/workbench/services/remote/common/remoteExtensionsScanner.ts +++ b/src/vs/workbench/services/remote/common/remoteExtensionsScanner.ts @@ -15,6 +15,7 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ import { ILogService } from 'vs/platform/log/common/log'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IActiveLanguagePackService } from 'vs/workbench/services/localization/common/locale'; +import { IWorkbenchExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; class RemoteExtensionsScannerService implements IRemoteExtensionsScannerService { @@ -25,8 +26,9 @@ class RemoteExtensionsScannerService implements IRemoteExtensionsScannerService @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, @IRemoteUserDataProfilesService private readonly remoteUserDataProfilesService: IRemoteUserDataProfilesService, + @IActiveLanguagePackService private readonly activeLanguagePackService: IActiveLanguagePackService, + @IWorkbenchExtensionManagementService private readonly extensionManagementService: IWorkbenchExtensionManagementService, @ILogService private readonly logService: ILogService, - @IActiveLanguagePackService private readonly activeLanguagePackService: IActiveLanguagePackService ) { } whenExtensionsReady(): Promise { @@ -42,7 +44,13 @@ class RemoteExtensionsScannerService implements IRemoteExtensionsScannerService return await this.withChannel( async (channel) => { const profileLocation = this.userDataProfileService.currentProfile.isDefault ? undefined : (await this.remoteUserDataProfilesService.getRemoteProfile(this.userDataProfileService.currentProfile)).extensionsResource; - const scannedExtensions = await channel.call('scanExtensions', [platform.language, profileLocation, this.environmentService.extensionDevelopmentLocationURI, languagePack]); + const scannedExtensions = await channel.call('scanExtensions', [ + platform.language, + profileLocation, + this.extensionManagementService.getInstalledWorkspaceExtensionLocations(), + this.environmentService.extensionDevelopmentLocationURI, + languagePack + ]); scannedExtensions.forEach((extension) => { extension.extensionLocation = URI.revive(extension.extensionLocation); }); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 8974dc8ae57..f6edde59ddb 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -2193,6 +2193,7 @@ export class TestWorkbenchExtensionManagementService implements IWorkbenchExtens toggleAppliationScope(): Promise { throw new Error('Not Supported'); } installExtensionsFromProfile(): Promise { throw new Error('Not Supported'); } whenProfileChanged(from: IUserDataProfile, to: IUserDataProfile): Promise { throw new Error('Not Supported'); } + getInstalledWorkspaceExtensionLocations(): URI[] { throw new Error('Method not implemented.'); } getInstalledWorkspaceExtensions(): Promise { throw new Error('Method not implemented.'); } installResourceExtension(): Promise { throw new Error('Method not implemented.'); } getExtensions(): Promise { throw new Error('Method not implemented.'); } From 528d13bffcae552e2c44ec85de7c94db87771a97 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 14 May 2024 17:41:34 +0200 Subject: [PATCH 163/357] report extension version (#212705) --- .../common/extensionManagementUtil.ts | 2 ++ .../extensionManagement/node/extensionDownloader.ts | 2 +- .../node/extensionSignatureVerificationService.ts | 9 +++++++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts b/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts index e7e7bab19e6..935f836438f 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts @@ -120,6 +120,7 @@ export function getLocalExtensionTelemetryData(extension: ILocalExtension): any "GalleryExtensionTelemetryData" : { "id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "name": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "version": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "galleryId": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "publisherId": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "publisherName": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, @@ -136,6 +137,7 @@ export function getGalleryExtensionTelemetryData(extension: IGalleryExtension): return { id: new TelemetryTrustedValue(extension.identifier.id), name: new TelemetryTrustedValue(extension.name), + version: extension.version, galleryId: extension.identifier.uuid, publisherId: extension.publisherId, publisherName: extension.publisher, diff --git a/src/vs/platform/extensionManagement/node/extensionDownloader.ts b/src/vs/platform/extensionManagement/node/extensionDownloader.ts index e8d8e6b1159..5607a6d84e7 100644 --- a/src/vs/platform/extensionManagement/node/extensionDownloader.ts +++ b/src/vs/platform/extensionManagement/node/extensionDownloader.ts @@ -74,7 +74,7 @@ export class ExtensionsDownloader extends Disposable { } try { - verificationStatus = await this.extensionSignatureVerificationService.verify(extension.identifier.id, location.fsPath, signatureArchiveLocation.fsPath, clientTargetPlatform); + verificationStatus = await this.extensionSignatureVerificationService.verify(extension, location.fsPath, signatureArchiveLocation.fsPath, clientTargetPlatform); } catch (error) { verificationStatus = (error as ExtensionSignatureVerificationError).code; if (verificationStatus === ExtensionSignatureVerificationCode.PackageIsInvalidZip || verificationStatus === ExtensionSignatureVerificationCode.SignatureArchiveIsInvalidZip) { diff --git a/src/vs/platform/extensionManagement/node/extensionSignatureVerificationService.ts b/src/vs/platform/extensionManagement/node/extensionSignatureVerificationService.ts index 1bfe311f29f..2b8f82ffbd2 100644 --- a/src/vs/platform/extensionManagement/node/extensionSignatureVerificationService.ts +++ b/src/vs/platform/extensionManagement/node/extensionSignatureVerificationService.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { getErrorMessage } from 'vs/base/common/errors'; +import { IGalleryExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; import { TargetPlatform } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ILogService, LogLevel } from 'vs/platform/log/common/log'; @@ -27,7 +28,7 @@ export interface IExtensionSignatureVerificationService { * @throws { ExtensionSignatureVerificationError } An error with a code indicating the validity, integrity, or trust issue * found during verification or a more fundamental issue (e.g.: a required dependency was not found). */ - verify(extensionId: string, vsixFilePath: string, signatureArchiveFilePath: string, clientTargetPlatform?: TargetPlatform): Promise; + verify(extension: IGalleryExtension, vsixFilePath: string, signatureArchiveFilePath: string, clientTargetPlatform?: TargetPlatform): Promise; } declare module vsceSign { @@ -107,8 +108,9 @@ export class ExtensionSignatureVerificationService implements IExtensionSignatur return this.moduleLoadingPromise; } - public async verify(extensionId: string, vsixFilePath: string, signatureArchiveFilePath: string, clientTargetPlatform?: TargetPlatform): Promise { + public async verify(extension: IGalleryExtension, vsixFilePath: string, signatureArchiveFilePath: string, clientTargetPlatform?: TargetPlatform): Promise { let module: typeof vsceSign; + const extensionId = extension.identifier.id; try { module = await this.vsceSign(); @@ -141,6 +143,7 @@ export class ExtensionSignatureVerificationService implements IExtensionSignatur owner: 'sandy081'; comment: 'Extension signature verification event'; extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'extension identifier' }; + extensionVersion: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'extension version' }; code: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'result code of the verification' }; duration: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; 'isMeasurement': true; comment: 'amount of time taken to verify the signature' }; didExecute: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'whether the verification was executed' }; @@ -148,6 +151,7 @@ export class ExtensionSignatureVerificationService implements IExtensionSignatur }; type ExtensionSignatureVerificationEvent = { extensionId: string; + extensionVersion: string; code: string; duration: number; didExecute: boolean; @@ -155,6 +159,7 @@ export class ExtensionSignatureVerificationService implements IExtensionSignatur }; this.telemetryService.publicLog2('extensionsignature:verification', { extensionId, + extensionVersion: extension.version, code: result.code, duration, didExecute: result.didExecute, From e1c99ae22e03e6815c8af49c3eb242df935b61bc Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Tue, 14 May 2024 09:11:17 -0700 Subject: [PATCH 164/357] Fix chat view name (#212646) * Fix chat view name * Fix --- .../contrib/chat/browser/chatParticipantContributions.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts b/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts index 551fcbea8e7..07e458fdca2 100644 --- a/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts @@ -265,12 +265,13 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { private registerDefaultParticipantView(defaultParticipantDescriptor: IRawChatParticipantContribution): IDisposable { // Register View + const name = defaultParticipantDescriptor.fullName ?? defaultParticipantDescriptor.name; const viewDescriptor: IViewDescriptor[] = [{ id: CHAT_VIEW_ID, containerIcon: this._viewContainer.icon, containerTitle: this._viewContainer.title.value, singleViewPaneContainerTitle: this._viewContainer.title.value, - name: { value: defaultParticipantDescriptor.name, original: defaultParticipantDescriptor.name }, + name: { value: name, original: name }, canToggleVisibility: false, canMoveView: true, ctorDescriptor: new SyncDescriptor(ChatViewPane), From cf8e443ca87384fb73f3bdcb2e1c55808e8d27e5 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Tue, 14 May 2024 18:21:11 +0200 Subject: [PATCH 165/357] one more round of API todos (#212710) --- .../vscode.proposed.chatParticipant.d.ts | 4 ++++ .../vscode.proposed.languageModels.d.ts | 19 ++++++++++++------- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/vscode-dts/vscode.proposed.chatParticipant.d.ts b/src/vscode-dts/vscode.proposed.chatParticipant.d.ts index ba74d3726de..66e943c4330 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipant.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipant.d.ts @@ -87,6 +87,7 @@ declare module 'vscode' { * For example, if the response terminated after sending part of a triple-backtick code block, then the editor will * render it as a complete code block. */ + // TODO@API: consider to have this always on, the presence of an error is a good indicator responseIsIncomplete?: boolean; /** @@ -243,6 +244,7 @@ declare module 'vscode' { * The name of the reference. * TODO@API should name be provided at all, or only ID? */ + // TODO@API nuke it, add when needed readonly name: string; /** @@ -290,6 +292,7 @@ declare module 'vscode' { * in the prompt. That means the last reference in the prompt is the first in this list. This simplifies * string-manipulation of the prompt. */ + // TODO@API: name ChatRequestReference, ChatPromptReference readonly references: readonly ChatValueReference[]; } @@ -298,6 +301,7 @@ declare module 'vscode' { * which will be rendered in an appropriate way in the chat view. A participant can use the helper method for the type of content it wants to return, or it * can instantiate a {@link ChatResponsePart} and use the generic {@link ChatResponseStream.push} method to return it. */ + // TODO@API make them return void export interface ChatResponseStream { /** * Push a markdown part to this stream. Short-hand for diff --git a/src/vscode-dts/vscode.proposed.languageModels.d.ts b/src/vscode-dts/vscode.proposed.languageModels.d.ts index c7600dc663b..68d2b2f4705 100644 --- a/src/vscode-dts/vscode.proposed.languageModels.d.ts +++ b/src/vscode-dts/vscode.proposed.languageModels.d.ts @@ -74,6 +74,7 @@ declare module 'vscode' { * @see {@link LanguageModelAccess.chatRequest} */ // TODO@API add something like `modelResult: Thenable<{ [name: string]: any }>` + // TODO@API: add a StopReason-enum that's also used in LanguageModelChat export interface LanguageModelChatResponse { /** @@ -95,7 +96,11 @@ declare module 'vscode' { * console.error(e); * } * ``` + * + * To cancel the stream, the consumer can {@link CancellationTokenSource.cancel cancel} the token that was used to make the request + * or break from the for-loop. */ + // TODO@API rename: text stream: AsyncIterable; } @@ -105,6 +110,12 @@ declare module 'vscode' { * @see {@link lm.selectChatModels} */ export interface LanguageModelChat { + + /** + * Human-readable name of the language model. + */ + readonly name: string; + /** * Opaque identifier of the language model. */ @@ -116,11 +127,6 @@ declare module 'vscode' { */ readonly vendor: string; - /** - * Human-readable name of the language model. - */ - readonly name: string; - /** * Opaque family-name of the language model. Values might be `gpt-3.5-turbo`, `gpt4`, `phi2`, or `llama` * but they are defined by extensions contributing languages and subject to change. @@ -141,6 +147,7 @@ declare module 'vscode' { /** * The maximum number of tokens that a model can generate in a single response. */ + // TODO@API leave it out for now readonly maxOutputTokens: number; /** @@ -317,8 +324,6 @@ declare module 'vscode' { * @return `true` if a request can be made, `false` if not, `undefined` if the language * model does not exist or consent hasn't been asked for. */ - // TODO@API applies to chat and embeddings models - // TODO@API name: canUse, hasAccess? canSendRequest(chat: LanguageModelChat): boolean | undefined; } From 95348fb8d0d634661bbaca427721c70c72601528 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Tue, 14 May 2024 18:50:34 +0200 Subject: [PATCH 166/357] Allow editor actions on inactive editor groups (#212715) * Allow editor actions on both sides * support for single tabs --- .../workbench/browser/parts/editor/editor.ts | 2 + .../browser/parts/editor/editorGroupView.ts | 47 ++++++++++++++++--- .../parts/editor/multiEditorTabsControl.ts | 2 +- .../parts/editor/singleEditorTabsControl.ts | 2 +- .../browser/workbench.contribution.ts | 5 ++ src/vs/workbench/common/editor.ts | 1 + 6 files changed, 51 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/editor.ts b/src/vs/workbench/browser/parts/editor/editor.ts index f2a1a929bde..6b9ad992bd4 100644 --- a/src/vs/workbench/browser/parts/editor/editor.ts +++ b/src/vs/workbench/browser/parts/editor/editor.ts @@ -32,6 +32,7 @@ export const DEFAULT_EDITOR_PART_OPTIONS: IEditorPartOptions = { tabActionLocation: 'right', tabActionCloseVisibility: true, tabActionUnpinVisibility: true, + alwaysShowEditorActions: false, tabSizing: 'fit', tabSizingFixedMinWidth: 50, tabSizingFixedMaxWidth: 160, @@ -121,6 +122,7 @@ function validateEditorPartOptions(options: IEditorPartOptions): IEditorPartOpti 'highlightModifiedTabs': new BooleanVerifier(DEFAULT_EDITOR_PART_OPTIONS['highlightModifiedTabs']), 'tabActionCloseVisibility': new BooleanVerifier(DEFAULT_EDITOR_PART_OPTIONS['tabActionCloseVisibility']), 'tabActionUnpinVisibility': new BooleanVerifier(DEFAULT_EDITOR_PART_OPTIONS['tabActionUnpinVisibility']), + 'alwaysShowEditorActions': new BooleanVerifier(DEFAULT_EDITOR_PART_OPTIONS['alwaysShowEditorActions']), 'pinnedTabsOnSeparateRow': new BooleanVerifier(DEFAULT_EDITOR_PART_OPTIONS['pinnedTabsOnSeparateRow']), 'focusRecentEditorAfterClose': new BooleanVerifier(DEFAULT_EDITOR_PART_OPTIONS['focusRecentEditorAfterClose']), 'showIcons': new BooleanVerifier(DEFAULT_EDITOR_PART_OPTIONS['showIcons']), diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index e95a07c45ff..4dc40e9aba9 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -5,8 +5,8 @@ import 'vs/css!./media/editorgroupview'; import { EditorGroupModel, IEditorOpenOptions, IGroupModelChangeEvent, ISerializedEditorGroupModel, isGroupEditorCloseEvent, isGroupEditorOpenEvent, isSerializedEditorGroupModel } from 'vs/workbench/common/editor/editorGroupModel'; -import { GroupIdentifier, CloseDirection, IEditorCloseEvent, IEditorPane, SaveReason, IEditorPartOptionsChangeEvent, EditorsOrder, IVisibleEditorPane, EditorResourceAccessor, EditorInputCapabilities, IUntypedEditorInput, DEFAULT_EDITOR_ASSOCIATION, SideBySideEditor, EditorCloseContext, IEditorWillMoveEvent, IEditorWillOpenEvent, IMatchEditorOptions, GroupModelChangeKind, IActiveEditorChangeEvent, IFindEditorOptions, IToolbarActions } from 'vs/workbench/common/editor'; -import { ActiveEditorGroupLockedContext, ActiveEditorDirtyContext, EditorGroupEditorsCountContext, ActiveEditorStickyContext, ActiveEditorPinnedContext, ActiveEditorLastInGroupContext, ActiveEditorFirstInGroupContext, ResourceContextKey, applyAvailableEditorIds, ActiveEditorAvailableEditorIdsContext, ActiveEditorCanSplitInGroupContext, SideBySideEditorActiveContext, MultipleEditorsSelectedContext, TwoEditorsSelectedContext } from 'vs/workbench/common/contextkeys'; +import { GroupIdentifier, CloseDirection, IEditorCloseEvent, IEditorPane, SaveReason, IEditorPartOptionsChangeEvent, EditorsOrder, IVisibleEditorPane, EditorResourceAccessor, EditorInputCapabilities, IUntypedEditorInput, DEFAULT_EDITOR_ASSOCIATION, SideBySideEditor, EditorCloseContext, IEditorWillMoveEvent, IEditorWillOpenEvent, IMatchEditorOptions, GroupModelChangeKind, IActiveEditorChangeEvent, IFindEditorOptions, IToolbarActions, TEXT_DIFF_EDITOR_ID } from 'vs/workbench/common/editor'; +import { ActiveEditorGroupLockedContext, ActiveEditorDirtyContext, EditorGroupEditorsCountContext, ActiveEditorStickyContext, ActiveEditorPinnedContext, ActiveEditorLastInGroupContext, ActiveEditorFirstInGroupContext, ResourceContextKey, applyAvailableEditorIds, ActiveEditorAvailableEditorIdsContext, ActiveEditorCanSplitInGroupContext, SideBySideEditorActiveContext, MultipleEditorsSelectedContext, TwoEditorsSelectedContext, TextCompareEditorVisibleContext, TextCompareEditorActiveContext, ActiveEditorContext, ActiveEditorReadonlyContext, ActiveEditorCanRevertContext, ActiveEditorCanToggleReadonlyContext, ActiveCompareEditorCanSwapContext } from 'vs/workbench/common/contextkeys'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { Emitter, Relay } from 'vs/base/common/event'; @@ -56,6 +56,8 @@ import { EditorTitleControl } from 'vs/workbench/browser/parts/editor/editorTitl import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { IEditorResolverService } from 'vs/workbench/services/editor/common/editorResolverService'; import { IHostService } from 'vs/workbench/services/host/browser/host'; +import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; +import { FileSystemProviderCapabilities, IFileService } from 'vs/platform/files/common/files'; export class EditorGroupView extends Themable implements IEditorGroupView { @@ -157,7 +159,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView { @ILogService private readonly logService: ILogService, @IEditorResolverService private readonly editorResolverService: IEditorResolverService, @IHostService private readonly hostService: IHostService, - @IDialogService private readonly dialogService: IDialogService + @IDialogService private readonly dialogService: IDialogService, + @IFileService private readonly fileService: IFileService ) { super(themeService); @@ -255,6 +258,14 @@ export class EditorGroupView extends Themable implements IEditorGroupView { const multipleEditorsSelectedContext = MultipleEditorsSelectedContext.bindTo(this.contextKeyService); const twoEditorsSelectedContext = TwoEditorsSelectedContext.bindTo(this.contextKeyService); + const groupActiveEditorContext = ActiveEditorContext.bindTo(this.scopedContextKeyService); + const groupActiveEditorIsReadonly = ActiveEditorReadonlyContext.bindTo(this.scopedContextKeyService); + const groupActiveEditorCanRevert = ActiveEditorCanRevertContext.bindTo(this.scopedContextKeyService); + const groupActiveEditorCanToggleReadonly = ActiveEditorCanToggleReadonlyContext.bindTo(this.scopedContextKeyService); + const groupActiveCompareEditorCanSwap = ActiveCompareEditorCanSwapContext.bindTo(this.scopedContextKeyService); + const groupTextCompareEditorVisibleContext = TextCompareEditorVisibleContext.bindTo(this.scopedContextKeyService); + const groupTextCompareEditorActiveContext = TextCompareEditorActiveContext.bindTo(this.scopedContextKeyService); + const groupActiveEditorAvailableEditorIds = ActiveEditorAvailableEditorIdsContext.bindTo(this.scopedContextKeyService); const groupActiveEditorCanSplitInGroupContext = ActiveEditorCanSplitInGroupContext.bindTo(this.scopedContextKeyService); const sideBySideEditorContext = SideBySideEditorActiveContext.bindTo(this.scopedContextKeyService); @@ -266,22 +277,46 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.scopedContextKeyService.bufferChangeEvents(() => { const activeEditor = this.activeEditor; + const activeEditorPane = this.activeEditorPane; this.resourceContext.set(EditorResourceAccessor.getOriginalUri(activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY })); applyAvailableEditorIds(groupActiveEditorAvailableEditorIds, activeEditor, this.editorResolverService); - groupActiveEditorCanSplitInGroupContext.set(activeEditor ? activeEditor.hasCapability(EditorInputCapabilities.CanSplitInGroup) : false); - sideBySideEditorContext.set(activeEditor?.typeId === SideBySideEditorInput.ID); - if (activeEditor) { + groupActiveEditorCanSplitInGroupContext.set(activeEditor.hasCapability(EditorInputCapabilities.CanSplitInGroup)); + sideBySideEditorContext.set(activeEditor.typeId === SideBySideEditorInput.ID); + groupActiveEditorDirtyContext.set(activeEditor.isDirty() && !activeEditor.isSaving()); activeEditorListener.value = activeEditor.onDidChangeDirty(() => { groupActiveEditorDirtyContext.set(activeEditor.isDirty() && !activeEditor.isSaving()); }); } else { + groupActiveEditorCanSplitInGroupContext.set(false); + sideBySideEditorContext.set(false); groupActiveEditorDirtyContext.set(false); } + + if (activeEditorPane) { + groupActiveEditorContext.set(activeEditorPane.getId()); + groupActiveEditorCanRevert.set(!activeEditorPane.input.hasCapability(EditorInputCapabilities.Untitled)); + groupActiveEditorIsReadonly.set(!!activeEditorPane.input.isReadonly()); + + const primaryEditorResource = EditorResourceAccessor.getOriginalUri(activeEditorPane.input, { supportSideBySide: SideBySideEditor.PRIMARY }); + const secondaryEditorResource = EditorResourceAccessor.getOriginalUri(activeEditorPane.input, { supportSideBySide: SideBySideEditor.SECONDARY }); + groupActiveCompareEditorCanSwap.set(activeEditorPane.input instanceof DiffEditorInput && !activeEditorPane.input.original.isReadonly() && !!primaryEditorResource && (this.fileService.hasProvider(primaryEditorResource) || primaryEditorResource.scheme === Schemas.untitled) && !!secondaryEditorResource && (this.fileService.hasProvider(secondaryEditorResource) || secondaryEditorResource.scheme === Schemas.untitled)); + groupActiveEditorCanToggleReadonly.set(!!primaryEditorResource && this.fileService.hasProvider(primaryEditorResource) && !this.fileService.hasCapability(primaryEditorResource, FileSystemProviderCapabilities.Readonly)); + + const activePaneDiffEditor = activeEditorPane?.getId() === TEXT_DIFF_EDITOR_ID; + groupTextCompareEditorActiveContext.set(activePaneDiffEditor); + groupTextCompareEditorVisibleContext.set(activePaneDiffEditor); + } else { + groupActiveEditorContext.reset(); + groupActiveEditorCanRevert.reset(); + groupActiveEditorIsReadonly.reset(); + groupActiveCompareEditorCanSwap.reset(); + groupActiveEditorCanToggleReadonly.reset(); + } }); }; diff --git a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts index e5d654c5f01..45b24ac116c 100644 --- a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts @@ -1693,7 +1693,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { // Inactive: only show "Unlock" and secondary actions else { return { - primary: editorActions.primary.filter(action => action.id === UNLOCK_GROUP_COMMAND_ID), + primary: this.groupsView.partOptions.alwaysShowEditorActions ? editorActions.primary : editorActions.primary.filter(action => action.id === UNLOCK_GROUP_COMMAND_ID), secondary: editorActions.secondary }; } diff --git a/src/vs/workbench/browser/parts/editor/singleEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/singleEditorTabsControl.ts index 8e69d2ab994..97f96a7d948 100644 --- a/src/vs/workbench/browser/parts/editor/singleEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/singleEditorTabsControl.ts @@ -355,7 +355,7 @@ export class SingleEditorTabsControl extends EditorTabsControl { // Inactive: only show "Close, "Unlock" and secondary actions else { return { - primary: editorActions.primary.filter(action => action.id === CLOSE_EDITOR_COMMAND_ID || action.id === UNLOCK_GROUP_COMMAND_ID), + primary: this.groupsView.partOptions.alwaysShowEditorActions ? editorActions.primary : editorActions.primary.filter(action => action.id === CLOSE_EDITOR_COMMAND_ID || action.id === UNLOCK_GROUP_COMMAND_ID), secondary: editorActions.secondary }; } diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index 5151e6228eb..41a4b4041d3 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -61,6 +61,11 @@ const registry = Registry.as(ConfigurationExtensions.Con 'markdownDescription': localize('editorActionsLocation', "Controls where the editor actions are shown."), 'default': 'default' }, + 'workbench.editor.alwaysShowEditorActions': { + 'type': 'boolean', + 'markdownDescription': localize('alwaysShowEditorActions', "Controls wheater to always show the editor actions, even when the editor group is not active."), + 'default': false + }, 'workbench.editor.wrapTabs': { 'type': 'boolean', 'markdownDescription': localize({ comment: ['{0}, {1} will be a setting name rendered as a link'], key: 'wrapTabs' }, "Controls whether tabs should be wrapped over multiple lines when exceeding available space or whether a scrollbar should appear instead. This value is ignored when {0} is not set to '{1}'.", '`#workbench.editor.showTabs#`', '`multiple`'), diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index c1e16b6929b..7d75370f67d 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -1208,6 +1208,7 @@ interface IEditorPartConfiguration { tabActionLocation?: 'left' | 'right'; tabActionCloseVisibility?: boolean; tabActionUnpinVisibility?: boolean; + alwaysShowEditorActions?: boolean; tabSizing?: 'fit' | 'shrink' | 'fixed'; tabSizingFixedMinWidth?: number; tabSizingFixedMaxWidth?: number; From 77e5788333b4d58426c80763e487c4826f144954 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Tue, 14 May 2024 19:02:32 +0200 Subject: [PATCH 167/357] Diff editor commands extract resource from action (#212719) Diff editor actions should properly extract context of action --- .../parts/editor/diffEditorCommands.ts | 47 ++++++++++--------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/diffEditorCommands.ts b/src/vs/workbench/browser/parts/editor/diffEditorCommands.ts index 1a84d021384..378552f35e0 100644 --- a/src/vs/workbench/browser/parts/editor/diffEditorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/diffEditorCommands.ts @@ -4,6 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { isEqual } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; import { localize, localize2 } from 'vs/nls'; import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; @@ -31,7 +33,7 @@ export function registerDiffEditorCommands(): void { weight: KeybindingWeight.WorkbenchContrib, when: TextCompareEditorVisibleContext, primary: KeyMod.Alt | KeyCode.F5, - handler: accessor => navigateInDiffEditor(accessor, true) + handler: (accessor, ...args) => navigateInDiffEditor(accessor, args, true) }); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { @@ -46,7 +48,7 @@ export function registerDiffEditorCommands(): void { weight: KeybindingWeight.WorkbenchContrib, when: TextCompareEditorVisibleContext, primary: KeyMod.Alt | KeyMod.Shift | KeyCode.F5, - handler: accessor => navigateInDiffEditor(accessor, false) + handler: (accessor, ...args) => navigateInDiffEditor(accessor, args, false) }); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { @@ -56,11 +58,12 @@ export function registerDiffEditorCommands(): void { } }); - function getActiveTextDiffEditor(accessor: ServicesAccessor): TextDiffEditor | undefined { + function getActiveTextDiffEditor(accessor: ServicesAccessor, args: any[]): TextDiffEditor | undefined { const editorService = accessor.get(IEditorService); + const resource = args.length > 0 && args[0] instanceof URI ? args[0] : undefined; for (const editor of [editorService.activeEditorPane, ...editorService.visibleEditorPanes]) { - if (editor instanceof TextDiffEditor) { + if (editor instanceof TextDiffEditor && (!resource || editor.input instanceof DiffEditorInput && isEqual(editor.input.primary.resource, resource))) { return editor; } } @@ -68,8 +71,8 @@ export function registerDiffEditorCommands(): void { return undefined; } - function navigateInDiffEditor(accessor: ServicesAccessor, next: boolean): void { - const activeTextDiffEditor = getActiveTextDiffEditor(accessor); + function navigateInDiffEditor(accessor: ServicesAccessor, args: any[], next: boolean): void { + const activeTextDiffEditor = getActiveTextDiffEditor(accessor, args); if (activeTextDiffEditor) { activeTextDiffEditor.getControl()?.goToDiff(next ? 'next' : 'previous'); @@ -82,8 +85,8 @@ export function registerDiffEditorCommands(): void { Toggle } - function focusInDiffEditor(accessor: ServicesAccessor, mode: FocusTextDiffEditorMode): void { - const activeTextDiffEditor = getActiveTextDiffEditor(accessor); + function focusInDiffEditor(accessor: ServicesAccessor, args: any[], mode: FocusTextDiffEditorMode): void { + const activeTextDiffEditor = getActiveTextDiffEditor(accessor, args); if (activeTextDiffEditor) { switch (mode) { @@ -95,17 +98,17 @@ export function registerDiffEditorCommands(): void { break; case FocusTextDiffEditorMode.Toggle: if (activeTextDiffEditor.getControl()?.getModifiedEditor().hasWidgetFocus()) { - return focusInDiffEditor(accessor, FocusTextDiffEditorMode.Original); + return focusInDiffEditor(accessor, args, FocusTextDiffEditorMode.Original); } else { - return focusInDiffEditor(accessor, FocusTextDiffEditorMode.Modified); + return focusInDiffEditor(accessor, args, FocusTextDiffEditorMode.Modified); } } } } - function toggleDiffSideBySide(accessor: ServicesAccessor): void { + function toggleDiffSideBySide(accessor: ServicesAccessor, args: any[]): void { const configService = accessor.get(ITextResourceConfigurationService); - const activeTextDiffEditor = getActiveTextDiffEditor(accessor); + const activeTextDiffEditor = getActiveTextDiffEditor(accessor, args); const m = activeTextDiffEditor?.getControl()?.getModifiedEditor()?.getModel(); if (!m) { return; } @@ -115,9 +118,9 @@ export function registerDiffEditorCommands(): void { configService.updateValue(m.uri, key, !val); } - function toggleDiffIgnoreTrimWhitespace(accessor: ServicesAccessor): void { + function toggleDiffIgnoreTrimWhitespace(accessor: ServicesAccessor, args: any[]): void { const configService = accessor.get(ITextResourceConfigurationService); - const activeTextDiffEditor = getActiveTextDiffEditor(accessor); + const activeTextDiffEditor = getActiveTextDiffEditor(accessor, args); const m = activeTextDiffEditor?.getControl()?.getModifiedEditor()?.getModel(); if (!m) { return; } @@ -127,10 +130,10 @@ export function registerDiffEditorCommands(): void { configService.updateValue(m.uri, key, !val); } - async function swapDiffSides(accessor: ServicesAccessor): Promise { + async function swapDiffSides(accessor: ServicesAccessor, args: any[]): Promise { const editorService = accessor.get(IEditorService); - const diffEditor = getActiveTextDiffEditor(accessor); + const diffEditor = getActiveTextDiffEditor(accessor, args); const activeGroup = diffEditor?.group; const diffInput = diffEditor?.input; if (!diffEditor || typeof activeGroup === 'undefined' || !(diffInput instanceof DiffEditorInput) || !diffInput.modified.resource) { @@ -179,7 +182,7 @@ export function registerDiffEditorCommands(): void { weight: KeybindingWeight.WorkbenchContrib, when: undefined, primary: undefined, - handler: accessor => toggleDiffSideBySide(accessor) + handler: (accessor, ...args) => toggleDiffSideBySide(accessor, args) }); KeybindingsRegistry.registerCommandAndKeybindingRule({ @@ -187,7 +190,7 @@ export function registerDiffEditorCommands(): void { weight: KeybindingWeight.WorkbenchContrib, when: undefined, primary: undefined, - handler: accessor => focusInDiffEditor(accessor, FocusTextDiffEditorMode.Modified) + handler: (accessor, ...args) => focusInDiffEditor(accessor, args, FocusTextDiffEditorMode.Modified) }); KeybindingsRegistry.registerCommandAndKeybindingRule({ @@ -195,7 +198,7 @@ export function registerDiffEditorCommands(): void { weight: KeybindingWeight.WorkbenchContrib, when: undefined, primary: undefined, - handler: accessor => focusInDiffEditor(accessor, FocusTextDiffEditorMode.Original) + handler: (accessor, ...args) => focusInDiffEditor(accessor, args, FocusTextDiffEditorMode.Original) }); KeybindingsRegistry.registerCommandAndKeybindingRule({ @@ -203,7 +206,7 @@ export function registerDiffEditorCommands(): void { weight: KeybindingWeight.WorkbenchContrib, when: undefined, primary: undefined, - handler: accessor => focusInDiffEditor(accessor, FocusTextDiffEditorMode.Toggle) + handler: (accessor, ...args) => focusInDiffEditor(accessor, args, FocusTextDiffEditorMode.Toggle) }); KeybindingsRegistry.registerCommandAndKeybindingRule({ @@ -211,7 +214,7 @@ export function registerDiffEditorCommands(): void { weight: KeybindingWeight.WorkbenchContrib, when: undefined, primary: undefined, - handler: accessor => toggleDiffIgnoreTrimWhitespace(accessor) + handler: (accessor, ...args) => toggleDiffIgnoreTrimWhitespace(accessor, args) }); KeybindingsRegistry.registerCommandAndKeybindingRule({ @@ -219,7 +222,7 @@ export function registerDiffEditorCommands(): void { weight: KeybindingWeight.WorkbenchContrib, when: undefined, primary: undefined, - handler: accessor => swapDiffSides(accessor) + handler: (accessor, ...args) => swapDiffSides(accessor, args) }); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { From 35c4fb4b9369e7c2024a4753d428187379be2243 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 14 May 2024 20:10:12 +0200 Subject: [PATCH 168/357] validate downloaded assets and retry (#212722) --- .../node/extensionDownloader.ts | 118 +++++++++++++++--- .../node/extensionManagementService.ts | 58 +-------- .../node/extensionManagementUtil.ts | 38 ++++-- .../node/installGalleryExtensionTask.test.ts | 9 +- 4 files changed, 139 insertions(+), 84 deletions(-) diff --git a/src/vs/platform/extensionManagement/node/extensionDownloader.ts b/src/vs/platform/extensionManagement/node/extensionDownloader.ts index 5607a6d84e7..0ddae28ed93 100644 --- a/src/vs/platform/extensionManagement/node/extensionDownloader.ts +++ b/src/vs/platform/extensionManagement/node/extensionDownloader.ts @@ -13,16 +13,29 @@ import { isBoolean } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { Promises as FSPromises } from 'vs/base/node/pfs'; -import { CorruptZipMessage } from 'vs/base/node/zip'; +import { buffer, CorruptZipMessage } from 'vs/base/node/zip'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; import { ExtensionVerificationStatus, toExtensionManagementError } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService'; import { ExtensionManagementError, ExtensionManagementErrorCode, IExtensionGalleryService, IGalleryExtension, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionKey, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { fromExtractError } from 'vs/platform/extensionManagement/node/extensionManagementUtil'; import { ExtensionSignatureVerificationError, ExtensionSignatureVerificationCode, IExtensionSignatureVerificationService } from 'vs/platform/extensionManagement/node/extensionSignatureVerificationService'; import { TargetPlatform } from 'vs/platform/extensions/common/extensions'; import { IFileService, IFileStatWithMetadata } from 'vs/platform/files/common/files'; import { ILogService } from 'vs/platform/log/common/log'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; + +type RetryDownloadClassification = { + owner: 'sandy081'; + comment: 'Event reporting the retry of downloading'; + extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Extension Id' }; + attempts: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Number of Attempts' }; +}; +type RetryDownloadEvent = { + extensionId: string; + attempts: number; +}; export class ExtensionsDownloader extends Disposable { @@ -38,6 +51,7 @@ export class ExtensionsDownloader extends Disposable { @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, @IConfigurationService private readonly configurationService: IConfigurationService, @IExtensionSignatureVerificationService private readonly extensionSignatureVerificationService: IExtensionSignatureVerificationService, + @ITelemetryService private readonly telemetryService: ITelemetryService, @ILogService private readonly logService: ILogService, ) { super(); @@ -49,12 +63,7 @@ export class ExtensionsDownloader extends Disposable { async download(extension: IGalleryExtension, operation: InstallOperation, verifySignature: boolean, clientTargetPlatform?: TargetPlatform): Promise<{ readonly location: URI; readonly verificationStatus: ExtensionVerificationStatus }> { await this.cleanUpPromise; - const location = joinPath(this.extensionsDownloadDir, this.getName(extension)); - try { - await this.downloadFile(extension, location, location => this.extensionGalleryService.download(extension, location, operation)); - } catch (error) { - throw toExtensionManagementError(error, ExtensionManagementErrorCode.Download); - } + const location = await this.downloadVSIX(extension, operation); let verificationStatus: ExtensionVerificationStatus = false; @@ -109,16 +118,64 @@ export class ExtensionsDownloader extends Disposable { return isBoolean(value) ? value : true; } - private async downloadSignatureArchive(extension: IGalleryExtension): Promise { - await this.cleanUpPromise; - - const location = joinPath(this.extensionsDownloadDir, `.${generateUuid()}`); + private async downloadVSIX(extension: IGalleryExtension, operation: InstallOperation): Promise { try { - await this.extensionGalleryService.downloadSignatureArchive(extension, location); - } catch (error) { - throw toExtensionManagementError(error, ExtensionManagementErrorCode.DownloadSignature); + const location = joinPath(this.extensionsDownloadDir, this.getName(extension)); + const attempts = await this.doDownload(extension, 'vsix', async () => { + await this.downloadFile(extension, location, location => this.extensionGalleryService.download(extension, location, operation)); + try { + await this.validate(location.fsPath, 'extension/package.json'); + } catch (error) { + try { + await this.fileService.del(location); + } catch (e) { + this.logService.warn(`Error while deleting: ${location.path}`, getErrorMessage(e)); + } + throw error; + } + }, 2); + + if (attempts > 1) { + this.telemetryService.publicLog2('extensiongallery:downloadvsix:retry', { + extensionId: extension.identifier.id, + attempts + }); + } + + return location; + } catch (e) { + throw toExtensionManagementError(e, ExtensionManagementErrorCode.Download); + } + } + + private async downloadSignatureArchive(extension: IGalleryExtension): Promise { + try { + const location = joinPath(this.extensionsDownloadDir, `.${generateUuid()}`); + const attempts = await this.doDownload(extension, 'sigzip', async () => { + await this.extensionGalleryService.downloadSignatureArchive(extension, location); + try { + await this.validate(location.fsPath, '.signature.p7s'); + } catch (error) { + try { + await this.fileService.del(location); + } catch (e) { + this.logService.warn(`Error while deleting: ${location.path}`, getErrorMessage(e)); + } + throw error; + } + }, 2); + + if (attempts > 1) { + this.telemetryService.publicLog2('extensiongallery:downloadsigzip:retry', { + extensionId: extension.identifier.id, + attempts + }); + } + + return location; + } catch (e) { + throw toExtensionManagementError(e, ExtensionManagementErrorCode.DownloadSignature); } - return location; } private async downloadFile(extension: IGalleryExtension, location: URI, downloadFn: (location: URI) => Promise): Promise { @@ -148,10 +205,10 @@ export class ExtensionsDownloader extends Disposable { // Rename temp location to original await FSPromises.rename(tempLocation.fsPath, location.fsPath, 2 * 60 * 1000 /* Retry for 2 minutes */); } catch (error) { - try { - await this.fileService.del(tempLocation); - } catch (e) { /* ignore */ } - if (error.code === 'ENOTEMPTY') { + try { await this.fileService.del(tempLocation); } catch (e) { /* ignore */ } + let exists = false; + try { exists = await this.fileService.exists(location); } catch (e) { /* ignore */ } + if (exists) { this.logService.info(`Rename failed because the file was downloaded by another source. So ignoring renaming.`, extension.identifier.id, location.path); } else { this.logService.info(`Rename failed because of ${getErrorMessage(error)}. Deleted the file from downloaded location`, tempLocation.path); @@ -160,6 +217,29 @@ export class ExtensionsDownloader extends Disposable { } } + private async doDownload(extension: IGalleryExtension, name: string, downloadFn: () => Promise, retries: number): Promise { + let attempts = 1; + while (true) { + try { + await downloadFn(); + return attempts; + } catch (e) { + if (attempts++ > retries) { + throw e; + } + this.logService.warn(`Failed downloading ${name}. ${getErrorMessage(e)}. Retry again...`, extension.identifier.id); + } + } + } + + protected async validate(zipPath: string, filePath: string): Promise { + try { + await buffer(zipPath, filePath); + } catch (e) { + throw fromExtractError(e); + } + } + async delete(location: URI): Promise { await this.cleanUpPromise; await this.fileService.del(location); diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index ebbd598ef88..835f0ebc632 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -21,7 +21,7 @@ import { isBoolean, isUndefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import * as pfs from 'vs/base/node/pfs'; -import { extract, ExtractError, IFile, zip } from 'vs/base/node/zip'; +import { extract, IFile, zip } from 'vs/base/node/zip'; import * as nls from 'vs/nls'; import { IDownloadService } from 'vs/platform/download/common/download'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -37,7 +37,7 @@ import { IExtensionsProfileScannerService, IScannedProfileExtension } from 'vs/p import { IExtensionsScannerService, IScannedExtension, ScanOptions } from 'vs/platform/extensionManagement/common/extensionsScannerService'; import { ExtensionsDownloader } from 'vs/platform/extensionManagement/node/extensionDownloader'; import { ExtensionsLifecycle } from 'vs/platform/extensionManagement/node/extensionLifecycle'; -import { getManifest } from 'vs/platform/extensionManagement/node/extensionManagementUtil'; +import { fromExtractError, getManifest } from 'vs/platform/extensionManagement/node/extensionManagementUtil'; import { ExtensionsManifestCache } from 'vs/platform/extensionManagement/node/extensionsManifestCache'; import { DidChangeProfileExtensionsEvent, ExtensionsWatcher } from 'vs/platform/extensionManagement/node/extensionsWatcher'; import { ExtensionType, IExtension, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; @@ -289,7 +289,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi const key = ExtensionKey.create(extension).toString(); let installExtensionTask = this.installGalleryExtensionsTasks.get(key); if (!installExtensionTask) { - this.installGalleryExtensionsTasks.set(key, installExtensionTask = new InstallGalleryExtensionTask(manifest, extension, options, this.extensionsDownloader, this.extensionsScanner, this.uriIdentityService, this.userDataProfilesService, this.extensionsScannerService, this.extensionsProfileScannerService, this.logService, this.telemetryService)); + this.installGalleryExtensionsTasks.set(key, installExtensionTask = new InstallGalleryExtensionTask(manifest, extension, options, this.extensionsDownloader, this.extensionsScanner, this.uriIdentityService, this.userDataProfilesService, this.extensionsScannerService, this.extensionsProfileScannerService, this.logService)); installExtensionTask.waitUntilTaskIsFinished().finally(() => this.installGalleryExtensionsTasks.delete(key)); } return installExtensionTask; @@ -518,15 +518,7 @@ export class ExtensionsScanner extends Disposable { await extract(zipPath, tempLocation.fsPath, { sourcePath: 'extension', overwrite: true }, token); this.logService.info(`Extracted extension to ${extensionLocation}:`, extensionKey.id); } catch (e) { - let errorCode = ExtensionManagementErrorCode.Extract; - if (e instanceof ExtractError) { - if (e.type === 'CorruptZip') { - errorCode = ExtensionManagementErrorCode.CorruptZip; - } else if (e.type === 'Incomplete') { - errorCode = ExtensionManagementErrorCode.IncompleteZip; - } - } - throw toExtensionManagementError(e, errorCode); + throw fromExtractError(e); } try { @@ -912,7 +904,6 @@ export class InstallGalleryExtensionTask extends InstallExtensionTask { extensionsScannerService: IExtensionsScannerService, extensionsProfileScannerService: IExtensionsProfileScannerService, logService: ILogService, - private readonly telemetryService: ITelemetryService, ) { super(manifest, gallery.identifier, gallery, options, extensionsScanner, uriIdentityService, userDataProfilesService, extensionsScannerService, extensionsProfileScannerService, logService); } @@ -949,7 +940,7 @@ export class InstallGalleryExtensionTask extends InstallExtensionTask { return [local, metadata]; } - const { verificationStatus, location } = await this.download(metadata, token); + const { verificationStatus, location } = await this.extensionsDownloader.download(this.gallery, this._operation, !this.options.donotVerifySignature, this.options.context?.[EXTENSION_INSTALL_CLIENT_TARGET_PLATFORM_CONTEXT]); try { this._verificationStatus = verificationStatus; await this.validateManifest(location.fsPath); @@ -966,46 +957,9 @@ export class InstallGalleryExtensionTask extends InstallExtensionTask { } } - private async download(metadata: Metadata, token: CancellationToken): Promise<{ readonly location: URI; readonly verificationStatus: ExtensionVerificationStatus }> { - try { - return await this.extensionsDownloader.download(this.gallery, this._operation, !this.options.donotVerifySignature, this.options.context?.[EXTENSION_INSTALL_CLIENT_TARGET_PLATFORM_CONTEXT]); - } catch (error) { - this.logService.info(`Failed downloading. Retry again...`, this.gallery.identifier.id); - type RetryDownloadingVSIXClassification = { - owner: 'sandy081'; - comment: 'Event reporting the retry of downloading the VSIX'; - extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Extension Id' }; - succeeded?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Success value' }; - }; - type RetryDownloadingVSIXEvent = { - extensionId: string; - succeeded: boolean; - }; - try { - const result = await this.download(metadata, token); - this.telemetryService.publicLog2('extensiongallery:download:retry', { - extensionId: this.gallery.identifier.id, - succeeded: true - }); - return result; - } catch (error) { - this.telemetryService.publicLog2('extensiongallery:download:retry', { - extensionId: this.gallery.identifier.id, - succeeded: false - }); - throw error; - } - } - } - protected async validateManifest(zipPath: string): Promise { - try { - await getManifest(zipPath); - } catch (error) { - throw toExtensionManagementError(error, ExtensionManagementErrorCode.Invalid); - } + await getManifest(zipPath); } - } class InstallVSIXTask extends InstallExtensionTask { diff --git a/src/vs/platform/extensionManagement/node/extensionManagementUtil.ts b/src/vs/platform/extensionManagement/node/extensionManagementUtil.ts index 6d9a54272da..96118542408 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementUtil.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementUtil.ts @@ -3,17 +3,35 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { buffer } from 'vs/base/node/zip'; +import { buffer, ExtractError } from 'vs/base/node/zip'; import { localize } from 'vs/nls'; +import { toExtensionManagementError } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService'; +import { ExtensionManagementError, ExtensionManagementErrorCode } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; -export function getManifest(vsix: string): Promise { - return buffer(vsix, 'extension/package.json') - .then(buffer => { - try { - return JSON.parse(buffer.toString('utf8')); - } catch (err) { - throw new Error(localize('invalidManifest', "VSIX invalid: package.json is not a JSON file.")); - } - }); +export function fromExtractError(e: Error): ExtensionManagementError { + let errorCode = ExtensionManagementErrorCode.Extract; + if (e instanceof ExtractError) { + if (e.type === 'CorruptZip') { + errorCode = ExtensionManagementErrorCode.CorruptZip; + } else if (e.type === 'Incomplete') { + errorCode = ExtensionManagementErrorCode.IncompleteZip; + } + } + return toExtensionManagementError(e, errorCode); +} + +export async function getManifest(vsixPath: string): Promise { + let data; + try { + data = await buffer(vsixPath, 'extension/package.json'); + } catch (e) { + throw fromExtractError(e); + } + + try { + return JSON.parse(data.toString('utf8')); + } catch (err) { + throw new ExtensionManagementError(localize('invalidManifest', "VSIX invalid: package.json is not a JSON file."), ExtensionManagementErrorCode.Invalid); + } } diff --git a/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts b/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts index 92ac41e0cea..54b1f33b471 100644 --- a/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts +++ b/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts @@ -107,8 +107,7 @@ class TestInstallGalleryExtensionTask extends InstallGalleryExtensionTask { userDataProfilesService, extensionsScannerService, extensionsProfileScannerService, - logService, - NullTelemetryService + logService ); } @@ -125,6 +124,10 @@ class TestInstallGalleryExtensionTask extends InstallGalleryExtensionTask { protected override async validateManifest(): Promise { } } +class TestExtensionDownloader extends ExtensionsDownloader { + protected override async validate(): Promise { } +} + suite('InstallGalleryExtensionTask Tests', () => { const disposables = ensureNoDisposablesAreLeakedInTestSuite(); @@ -234,7 +237,7 @@ suite('InstallGalleryExtensionTask Tests', () => { }); instantiationService.stub(IConfigurationService, new TestConfigurationService(isBoolean(options.isSignatureVerificationEnabled) ? { extensions: { verifySignature: options.isSignatureVerificationEnabled } } : undefined)); instantiationService.stub(IExtensionSignatureVerificationService, new TestExtensionSignatureVerificationService(options.verificationResult)); - return disposables.add(instantiationService.createInstance(ExtensionsDownloader)); + return disposables.add(instantiationService.createInstance(TestExtensionDownloader)); } function aGalleryExtension(name: string, properties: Partial = {}, galleryExtensionProperties: any = {}, assets: Partial = {}): IGalleryExtension { From 146d9ed08b06926ada5bc6097948d49c1726fc4a Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 14 May 2024 20:11:12 +0200 Subject: [PATCH 169/357] report internalCode (#212724) --- .../node/extensionSignatureVerificationService.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/vs/platform/extensionManagement/node/extensionSignatureVerificationService.ts b/src/vs/platform/extensionManagement/node/extensionSignatureVerificationService.ts index 2b8f82ffbd2..b50638dbf6d 100644 --- a/src/vs/platform/extensionManagement/node/extensionSignatureVerificationService.ts +++ b/src/vs/platform/extensionManagement/node/extensionSignatureVerificationService.ts @@ -72,6 +72,7 @@ export const enum ExtensionSignatureVerificationCode { export interface ExtensionSignatureVerificationResult { readonly code: ExtensionSignatureVerificationCode; readonly didExecute: boolean; + readonly internalCode?: number; readonly output?: string; } @@ -145,6 +146,7 @@ export class ExtensionSignatureVerificationService implements IExtensionSignatur extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'extension identifier' }; extensionVersion: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'extension version' }; code: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'result code of the verification' }; + internalCode?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; 'isMeasurement': true; comment: 'internal code of the verification' }; duration: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; 'isMeasurement': true; comment: 'amount of time taken to verify the signature' }; didExecute: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'whether the verification was executed' }; clientTargetPlatform?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'target platform of the client' }; @@ -153,6 +155,7 @@ export class ExtensionSignatureVerificationService implements IExtensionSignatur extensionId: string; extensionVersion: string; code: string; + internalCode?: number; duration: number; didExecute: boolean; clientTargetPlatform?: string; @@ -161,6 +164,7 @@ export class ExtensionSignatureVerificationService implements IExtensionSignatur extensionId, extensionVersion: extension.version, code: result.code, + internalCode: result.internalCode, duration, didExecute: result.didExecute, clientTargetPlatform, From daec93b827222c24ac0fcdc45296efe956068a75 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Tue, 14 May 2024 11:12:28 -0700 Subject: [PATCH 170/357] Disable web ata for safari (#212726) --- .../package.nls.json | 2 +- .../src/extension.browser.ts | 27 ++++++++++++------- .../src/languageProvider.ts | 6 +++-- .../src/tsServer/serverProcess.browser.ts | 5 ++-- .../src/utils/platform.ts | 5 ++++ 5 files changed, 30 insertions(+), 15 deletions(-) diff --git a/extensions/typescript-language-features/package.nls.json b/extensions/typescript-language-features/package.nls.json index 332f69b7140..7fb5bae6ad1 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -217,7 +217,7 @@ "configuration.suggest.objectLiteralMethodSnippets.enabled": "Enable/disable snippet completions for methods in object literals.", "configuration.tsserver.web.projectWideIntellisense.enabled": "Enable/disable project-wide IntelliSense on web. Requires that VS Code is running in a trusted context.", "configuration.tsserver.web.projectWideIntellisense.suppressSemanticErrors": "Suppresses semantic errors on web even when project wide IntelliSense is enabled. This is always on when project wide IntelliSense is not enabled or available. See `#typescript.tsserver.web.projectWideIntellisense.enabled#`", - "configuration.tsserver.web.typeAcquisition.enabled": "Enable/disable package acquisition on the web. This enables IntelliSense for imported packages. Requires `#typescript.tsserver.web.projectWideIntellisense.enabled#`.", + "configuration.tsserver.web.typeAcquisition.enabled": "Enable/disable package acquisition on the web. This enables IntelliSense for imported packages. Requires `#typescript.tsserver.web.projectWideIntellisense.enabled#`. Currently not supported for Safari.", "configuration.tsserver.nodePath": "Run TS Server on a custom Node installation. This can be a path to a Node executable, or 'node' if you want VS Code to detect a Node installation.", "walkthroughs.nodejsWelcome.title": "Get started with JavaScript and Node.js", "walkthroughs.nodejsWelcome.description": "Make the most of Visual Studio Code's first-class JavaScript experience.", diff --git a/extensions/typescript-language-features/src/extension.browser.ts b/extensions/typescript-language-features/src/extension.browser.ts index 25a7669a326..2f6bd2127e1 100644 --- a/extensions/typescript-language-features/src/extension.browser.ts +++ b/extensions/typescript-language-features/src/extension.browser.ts @@ -25,7 +25,7 @@ import { ITypeScriptVersionProvider, TypeScriptVersion, TypeScriptVersionSource import { ActiveJsTsEditorTracker } from './ui/activeJsTsEditorTracker'; import { Disposable } from './utils/dispose'; import { getPackageInfo } from './utils/packageInfo'; -import { isWebAndHasSharedArrayBuffers } from './utils/platform'; +import { isWebAndHasSharedArrayBuffers, supportsReadableByteStreams } from './utils/platform'; class StaticVersionProvider implements ITypeScriptVersionProvider { @@ -101,14 +101,17 @@ export async function activate(context: vscode.ExtensionContext): Promise { context.subscriptions.push(lazilyActivateClient(lazyClientHost, pluginManager, activeJsTsEditorTracker, async () => { await startPreloadWorkspaceContentsIfNeeded(context, logger); })); - context.subscriptions.push(vscode.workspace.registerFileSystemProvider('vscode-global-typings', new MemFs(), { - isCaseSensitive: true, - isReadonly: false - })); - context.subscriptions.push(vscode.workspace.registerFileSystemProvider('vscode-node-modules', new AutoInstallerFs(), { - isCaseSensitive: true, - isReadonly: false - })); + + if (supportsReadableByteStreams()) { + context.subscriptions.push(vscode.workspace.registerFileSystemProvider('vscode-global-typings', new MemFs(), { + isCaseSensitive: true, + isReadonly: false + })); + context.subscriptions.push(vscode.workspace.registerFileSystemProvider('vscode-node-modules', new AutoInstallerFs(), { + isCaseSensitive: true, + isReadonly: false + })); + } return getExtensionApi(onCompletionAccepted.event, pluginManager); } @@ -131,7 +134,11 @@ async function startPreloadWorkspaceContentsIfNeeded(context: vscode.ExtensionCo const loader = new RemoteWorkspaceContentsPreloader(workspaceUri, logger); context.subscriptions.push(loader); - await loader.triggerPreload(); + try { + await loader.triggerPreload(); + } catch (error) { + console.error(error); + } })); } diff --git a/extensions/typescript-language-features/src/languageProvider.ts b/extensions/typescript-language-features/src/languageProvider.ts index f44ebcc1212..b09df40561b 100644 --- a/extensions/typescript-language-features/src/languageProvider.ts +++ b/extensions/typescript-language-features/src/languageProvider.ts @@ -18,7 +18,7 @@ import { ClientCapability } from './typescriptService'; import TypeScriptServiceClient from './typescriptServiceClient'; import TypingsStatus from './ui/typingsStatus'; import { Disposable } from './utils/dispose'; -import { isWeb, isWebAndHasSharedArrayBuffers } from './utils/platform'; +import { isWeb, isWebAndHasSharedArrayBuffers, supportsReadableByteStreams } from './utils/platform'; const validateSetting = 'validate.enable'; @@ -143,7 +143,9 @@ export default class LanguageProvider extends Disposable { } if (diagnosticsKind === DiagnosticKind.Semantic && isWeb()) { - if (!isWebAndHasSharedArrayBuffers() + if ( + !isWebAndHasSharedArrayBuffers() + || !supportsReadableByteStreams() // No ata. Will result in lots of false positives || this.client.configuration.webProjectWideIntellisenseSuppressSemanticErrors || !this.client.configuration.webProjectWideIntellisenseEnabled ) { diff --git a/extensions/typescript-language-features/src/tsServer/serverProcess.browser.ts b/extensions/typescript-language-features/src/tsServer/serverProcess.browser.ts index 8c5b8bfc527..71daf1fb0b6 100644 --- a/extensions/typescript-language-features/src/tsServer/serverProcess.browser.ts +++ b/extensions/typescript-language-features/src/tsServer/serverProcess.browser.ts @@ -8,12 +8,13 @@ import { ApiService, Requests } from '@vscode/sync-api-service'; import * as vscode from 'vscode'; import { TypeScriptServiceConfiguration } from '../configuration/configuration'; import { Logger } from '../logging/logger'; +import { supportsReadableByteStreams } from '../utils/platform'; import { FileWatcherManager } from './fileWatchingManager'; +import { NodeVersionManager } from './nodeManager'; import type * as Proto from './protocol/protocol'; import { TsServerLog, TsServerProcess, TsServerProcessFactory, TsServerProcessKind } from './server'; import { TypeScriptVersionManager } from './versionManager'; import { TypeScriptVersion } from './versionProvider'; -import { NodeVersionManager } from './nodeManager'; type BrowserWatchEvent = { type: 'watchDirectory' | 'watchFile'; @@ -50,7 +51,7 @@ export class WorkerServerProcessFactory implements TsServerProcessFactory { // Explicitly give TS Server its path so it can load local resources '--executingFilePath', tsServerPath, ]; - if (_configuration.webTypeAcquisitionEnabled) { + if (_configuration.webTypeAcquisitionEnabled && supportsReadableByteStreams()) { launchArgs.push('--experimentalTypeAcquisition'); } return new WorkerServerProcess(kind, tsServerPath, this._extensionUri, launchArgs, tsServerLog, this._logger); diff --git a/extensions/typescript-language-features/src/utils/platform.ts b/extensions/typescript-language-features/src/utils/platform.ts index a7bdd8f30ff..ba954f59da3 100644 --- a/extensions/typescript-language-features/src/utils/platform.ts +++ b/extensions/typescript-language-features/src/utils/platform.ts @@ -12,3 +12,8 @@ export function isWeb(): boolean { export function isWebAndHasSharedArrayBuffers(): boolean { return isWeb() && (globalThis as any)['crossOriginIsolated']; } + +export function supportsReadableByteStreams(): boolean { + return isWeb() && 'ReadableByteStreamController' in globalThis; +} + From 26dbd91bfa4377733af135bde41ca469d969b324 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 14 May 2024 11:56:44 -0700 Subject: [PATCH 171/357] testing: update coverage from UX feedback - Have coverage actions in the editor title by default - Coverage actions move into the toolbar when the toolbar is toggled on - Use a view action in the Test Coverage view for filtering data - Don't show %'s on directories in the Test Coverage view when filtered --- .../browser/codeCoverageDecorations.ts | 190 +++++++++++------- .../contrib/testing/browser/media/testing.css | 4 + .../testing/browser/testCoverageView.ts | 130 ++++-------- .../contrib/testing/common/constants.ts | 1 + .../contrib/testing/common/testCoverage.ts | 24 +-- .../testing/common/testCoverageService.ts | 6 + .../testing/common/testingContextKeys.ts | 1 + 7 files changed, 173 insertions(+), 183 deletions(-) diff --git a/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts b/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts index 9040875421a..67cc55a485d 100644 --- a/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts +++ b/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts @@ -11,13 +11,16 @@ import { Action } from 'vs/base/common/actions'; import { mapFindFirst } from 'vs/base/common/arraysFind'; import { assert, assertNever } from 'vs/base/common/assert'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Codicon } from 'vs/base/common/codicons'; import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Lazy } from 'vs/base/common/lazy'; import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { autorun, derived, observableFromEvent, observableValue } from 'vs/base/common/observable'; import { ThemeIcon } from 'vs/base/common/themables'; +import { isUriComponents, URI } from 'vs/base/common/uri'; import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, MouseTargetType, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; @@ -26,7 +29,9 @@ import { IModelDecorationOptions, InjectedTextCursorStops, InjectedTextOptions, import { localize, localize2 } from 'vs/nls'; import { Categories } from 'vs/platform/action/common/actionCommonCategories'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -35,7 +40,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; import { IQuickInputService, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; import * as coverUtils from 'vs/workbench/contrib/testing/browser/codeCoverageDisplayUtils'; -import { testingCoverageIcon, testingCoverageMissingBranch, testingFilterIcon, testingRerunIcon } from 'vs/workbench/contrib/testing/browser/icons'; +import { testingCoverageMissingBranch, testingCoverageReport, testingFilterIcon, testingRerunIcon } from 'vs/workbench/contrib/testing/browser/icons'; import { ManagedTestCoverageBars } from 'vs/workbench/contrib/testing/browser/testCoverageBars'; import { getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration'; import { TestCommandId } from 'vs/workbench/contrib/testing/common/constants'; @@ -538,7 +543,6 @@ class CoverageToolbarWidget extends Disposable implements IOverlayWidget { private readonly _domNode = dom.h('div.coverage-summary-widget', [ dom.h('div', [ dom.h('span.bars@bars'), - dom.h('span.stat@stat'), dom.h('span.toolbar@toolbar'), ]), ]); @@ -548,11 +552,10 @@ class CoverageToolbarWidget extends Disposable implements IOverlayWidget { constructor( private readonly editor: ICodeEditor, @IConfigurationService private readonly configurationService: IConfigurationService, - @IQuickInputService private readonly quickInputService: IQuickInputService, - @ITestCoverageService private readonly testCoverageService: ITestCoverageService, @IContextMenuService private readonly contextMenuService: IContextMenuService, @ITestService private readonly testService: ITestService, @IKeybindingService private readonly keybindingService: IKeybindingService, + @ICommandService private readonly commandService: ICommandService, @IInstantiationService instaService: IInstantiationService, ) { super(); @@ -611,70 +614,11 @@ class CoverageToolbarWidget extends Disposable implements IOverlayWidget { this.bars.setCoverageInfo(coverage); if (!coverage) { - return this.hide(); + this.hide(); + } else { + this.setActions(); + this.show(); } - - const displayStat = coverUtils.calculateDisplayedStat(coverage, getTestingConfiguration(this.configurationService, TestingConfigKeys.CoveragePercent)); - this._domNode.stat.innerText = localize('testing.percentCoverage', '{0} Coverage', coverUtils.displayPercent(displayStat)); - this.setActions(); - this.show(); - } - - private filterTest() { - const options = this.current?.perTestData ?? this.current?.isForTest?.parent.perTestData; - if (!options) { - return; - } - - const tests = [...options.values()]; - const commonPrefix = TestId.getLengthOfCommonPrefix(tests.length, i => tests[i].isForTest!.id); - const result = this.current!.fromResult; - const previousSelection = this.testCoverageService.filterToTest.get(); - - type TItem = { label: string; description?: string; item: FileCoverage | undefined }; - - const items: QuickPickInput[] = [ - { label: coverUtils.labels.allTests, item: undefined }, - { type: 'separator' }, - ...tests.map(item => ({ label: coverUtils.getLabelForItem(result, item.isForTest!.id, commonPrefix), description: coverUtils.labels.percentCoverage(item.tpc), item })), - ]; - - // These handle the behavior that reveals the start of coverage when the - // user picks from the quickpick. Scroll position is restored if the user - // exits without picking an item, or picks "all tets". - const scrollTop = this.editor.getScrollTop(); - const revealScrollCts = new MutableDisposable(); - - this.quickInputService.pick(items, { - activeItem: items.find((item): item is TItem => 'item' in item && item.item === this.current), - placeHolder: coverUtils.labels.pickShowCoverage, - onDidFocus: (entry) => { - if (!entry.item) { - revealScrollCts.clear(); - this.editor.setScrollTop(scrollTop); - this.testCoverageService.filterToTest.set(undefined, undefined); - } else { - const cts = revealScrollCts.value = new CancellationTokenSource(); - entry.item.details(cts.token).then( - details => { - const first = details.find(d => d.type === DetailType.Statement); - if (!cts.token.isCancellationRequested && first) { - this.editor.revealLineNearTop(first.location instanceof Position ? first.location.lineNumber : first.location.startLineNumber); - } - }, - () => { /* ignored */ } - ); - this.testCoverageService.filterToTest.set(entry.item.isForTest!.id, undefined); - } - }, - }).then(selected => { - if (!selected) { - this.editor.setScrollTop(scrollTop); - } - - revealScrollCts.dispose(); - this.testCoverageService.filterToTest.set(selected ? selected.item?.isForTest!.id : previousSelection, undefined); - }); } private setActions() { @@ -689,7 +633,7 @@ class CoverageToolbarWidget extends Disposable implements IOverlayWidget { CodeCoverageDecorations.showInline.get() ? localize('testing.hideInlineCoverage', 'Hide Inline Coverage') : localize('testing.showInlineCoverage', 'Show Inline Coverage'), - testingCoverageIcon, + testingCoverageReport, undefined, () => CodeCoverageDecorations.showInline.set(!CodeCoverageDecorations.showInline.get(), undefined), ); @@ -708,14 +652,14 @@ class CoverageToolbarWidget extends Disposable implements IOverlayWidget { coverUtils.labels.showingFilterFor(testItem.label), testingFilterIcon, undefined, - () => this.filterTest(), + () => this.commandService.executeCommand(TestCommandId.CoverageFilterToTestInEditor, this.current), )); } else if (coverage.perTestData?.size) { this.actionBar.push(new ActionWithIcon('perTestFilter', - localize('testing.coverageForTestAvailable', "{0} test(s) in this file", coverage.perTestData.size), + localize('testing.coverageForTestAvailable', "{0} test(s) ran code in this file", coverage.perTestData.size), testingFilterIcon, undefined, - () => this.filterTest(), + () => this.commandService.executeCommand(TestCommandId.CoverageFilterToTestInEditor, this.current), )); } @@ -784,13 +728,17 @@ registerAction2(class ToggleInlineCoverage extends Action2 { constructor() { super({ id: TOGGLE_INLINE_COMMAND_ID, - title: localize2('coverage.toggleInline', "Toggle Inline Coverage"), + title: localize2('coverage.toggleInline', "Show Inline Coverage"), category: Categories.Test, keybinding: { weight: KeybindingWeight.WorkbenchContrib, primary: KeyChord(KeyMod.CtrlCmd | KeyCode.Semicolon, KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyI), }, - precondition: TestingContextKeys.isTestCoverageOpen, + icon: testingCoverageReport, + menu: [ + { id: MenuId.CommandPalette, when: TestingContextKeys.isTestCoverageOpen }, + { id: MenuId.EditorTitle, when: ContextKeyExpr.and(TestingContextKeys.isTestCoverageOpen, TestingContextKeys.coverageToolbarEnabled.notEqualsTo(true)), group: 'navigation' }, + ] }); } @@ -803,18 +751,18 @@ registerAction2(class ToggleCoverageToolbar extends Action2 { constructor() { super({ id: TestCommandId.CoverageToggleToolbar, - title: localize2('testing.toggleToolbarTitle', "Toggle Coverage Toolbar"), + title: localize2('testing.toggleToolbarTitle', "Test Coverage Toolbar"), metadata: { description: localize2('testing.toggleToolbarDesc', 'Toggle the sticky coverage bar in the editor.') }, category: Categories.Test, toggled: { condition: TestingContextKeys.coverageToolbarEnabled, - title: localize('cmd.toggle2', "Toggle Coverage Toolbar"), }, menu: [ { id: MenuId.CommandPalette, when: TestingContextKeys.isTestCoverageOpen }, { id: MenuId.StickyScrollContext, when: TestingContextKeys.isTestCoverageOpen }, + { id: MenuId.EditorTitle, when: TestingContextKeys.isTestCoverageOpen, group: 'coverage@1' }, ] }); } @@ -826,6 +774,98 @@ registerAction2(class ToggleCoverageToolbar extends Action2 { } }); +registerAction2(class FilterCoverageToTestInEditor extends Action2 { + constructor() { + super({ + id: TestCommandId.CoverageFilterToTestInEditor, + title: localize2('testing.filterActionLabel', "Filter Coverage to Test"), + category: Categories.Test, + icon: Codicon.filter, + toggled: { + icon: Codicon.filterFilled, + condition: TestingContextKeys.isCoverageFilteredToTest, + }, + menu: [ + { id: MenuId.EditorTitle, when: ContextKeyExpr.and(TestingContextKeys.isTestCoverageOpen, TestingContextKeys.coverageToolbarEnabled.notEqualsTo(true)), group: 'navigation' }, + ] + }); + } + + run(accessor: ServicesAccessor, coverageOrUri?: FileCoverage | URI): void { + const testCoverageService = accessor.get(ITestCoverageService); + const quickInputService = accessor.get(IQuickInputService); + const activeEditor = accessor.get(ICodeEditorService).getActiveCodeEditor(); + let coverage: FileCoverage | undefined; + if (coverageOrUri instanceof FileCoverage) { + coverage = coverageOrUri; + } else if (isUriComponents(coverageOrUri)) { + coverage = testCoverageService.selected.get()?.getUri(URI.from(coverageOrUri)); + } else { + const uri = activeEditor?.getModel()?.uri; + coverage = uri && testCoverageService.selected.get()?.getUri(uri); + } + + if (!coverage || !(coverage.isForTest || coverage.perTestData?.size)) { + return; + } + + const options = coverage?.perTestData ?? coverage?.isForTest?.parent.perTestData; + if (!options) { + return; + } + + const tests = [...options.values()]; + const commonPrefix = TestId.getLengthOfCommonPrefix(tests.length, i => tests[i].isForTest!.id); + const result = coverage.fromResult; + const previousSelection = testCoverageService.filterToTest.get(); + + type TItem = { label: string; description?: string; item: FileCoverage | undefined }; + + const items: QuickPickInput[] = [ + { label: coverUtils.labels.allTests, item: undefined }, + { type: 'separator' }, + ...tests.map(item => ({ label: coverUtils.getLabelForItem(result, item.isForTest!.id, commonPrefix), description: coverUtils.labels.percentCoverage(item.tpc), item })), + ]; + + // These handle the behavior that reveals the start of coverage when the + // user picks from the quickpick. Scroll position is restored if the user + // exits without picking an item, or picks "all tets". + const scrollTop = activeEditor?.getScrollTop() || 0; + const revealScrollCts = new MutableDisposable(); + + quickInputService.pick(items, { + activeItem: items.find((item): item is TItem => 'item' in item && item.item === coverage), + placeHolder: coverUtils.labels.pickShowCoverage, + onDidFocus: (entry) => { + if (!entry.item) { + revealScrollCts.clear(); + activeEditor?.setScrollTop(scrollTop); + testCoverageService.filterToTest.set(undefined, undefined); + } else { + const cts = revealScrollCts.value = new CancellationTokenSource(); + entry.item.details(cts.token).then( + details => { + const first = details.find(d => d.type === DetailType.Statement); + if (!cts.token.isCancellationRequested && first) { + activeEditor?.revealLineNearTop(first.location instanceof Position ? first.location.lineNumber : first.location.startLineNumber); + } + }, + () => { /* ignored */ } + ); + testCoverageService.filterToTest.set(entry.item.isForTest!.id, undefined); + } + }, + }).then(selected => { + if (!selected) { + activeEditor?.setScrollTop(scrollTop); + } + + revealScrollCts.dispose(); + testCoverageService.filterToTest.set(selected ? selected.item?.isForTest!.id : previousSelection, undefined); + }); + } +}); + class ActionWithIcon extends Action { constructor(id: string, title: string, public readonly icon: ThemeIcon, enabled: boolean | undefined, run: () => void) { super(id, title, undefined, enabled, run); diff --git a/src/vs/workbench/contrib/testing/browser/media/testing.css b/src/vs/workbench/contrib/testing/browser/media/testing.css index 0e761d95c39..c08621d410b 100644 --- a/src/vs/workbench/contrib/testing/browser/media/testing.css +++ b/src/vs/workbench/contrib/testing/browser/media/testing.css @@ -363,6 +363,10 @@ /** -- coverage */ +.coverage-view-is-filtered > .pane-header > .actions { + display: block !important; +} + .test-coverage-list-item { display: flex; align-items: center; diff --git a/src/vs/workbench/contrib/testing/browser/testCoverageView.ts b/src/vs/workbench/contrib/testing/browser/testCoverageView.ts index 841903f8096..8e67eebd282 100644 --- a/src/vs/workbench/contrib/testing/browser/testCoverageView.ts +++ b/src/vs/workbench/contrib/testing/browser/testCoverageView.ts @@ -25,7 +25,6 @@ import { Range } from 'vs/editor/common/core/range'; import { localize, localize2 } from 'vs/nls'; import { Categories } from 'vs/platform/action/common/actionCommonCategories'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; -import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; @@ -48,11 +47,11 @@ import { testingStatesToIcons, testingWasCovered } from 'vs/workbench/contrib/te import { CoverageBarSource, ManagedTestCoverageBars } from 'vs/workbench/contrib/testing/browser/testCoverageBars'; import { TestCommandId, Testing } from 'vs/workbench/contrib/testing/common/constants'; import { onObservableChange } from 'vs/workbench/contrib/testing/common/observableUtils'; -import { ComputedFileCoverage, FileCoverage, TestCoverage, getTotalCoveragePercent } from 'vs/workbench/contrib/testing/common/testCoverage'; +import { BypassedFileCoverage, ComputedFileCoverage, FileCoverage, TestCoverage, getTotalCoveragePercent } from 'vs/workbench/contrib/testing/common/testCoverage'; import { ITestCoverageService } from 'vs/workbench/contrib/testing/common/testCoverageService'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; -import { CoverageDetails, DetailType, ICoverageCount, IDeclarationCoverage, ITestItem, TestResultState } from 'vs/workbench/contrib/testing/common/testTypes'; +import { CoverageDetails, DetailType, ICoverageCount, IDeclarationCoverage, TestResultState } from 'vs/workbench/contrib/testing/common/testTypes'; import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; const enum CoverageSortOrder { @@ -96,6 +95,13 @@ export class TestCoverageView extends ViewPane { this.tree.clear(); } })); + + this._register(autorun(reader => { + this.element.classList.toggle( + 'coverage-view-is-filtered', + !!this.coverageService.filterToTest.read(reader), + ); + })); } protected override layoutBody(height: number, width: number): void { @@ -196,16 +202,9 @@ class LoadingDetails { public readonly label = localize('loadingCoverageDetails', "Loading Coverage Details..."); } -class PerTestCoverageSwitcher { - public readonly id = String(fnNodeId++); - public readonly label = localize('changePerTestFilter', 'Click to change test filtering'); - - constructor(public readonly currentFilter: ITestItem | undefined) { } -} - /** Type of nodes returned from {@link TestCoverage}. Note: value is *always* defined. */ type TestCoverageFileNode = IPrefixTreeNode; -type CoverageTreeElement = TestCoverageFileNode | DeclarationCoverageNode | LoadingDetails | RevealUncoveredDeclarations | PerTestCoverageSwitcher; +type CoverageTreeElement = TestCoverageFileNode | DeclarationCoverageNode | LoadingDetails | RevealUncoveredDeclarations; const isFileCoverage = (c: CoverageTreeElement): c is TestCoverageFileNode => typeof c === 'object' && 'value' in c; const isDeclarationCoverage = (c: CoverageTreeElement): c is DeclarationCoverageNode => c instanceof DeclarationCoverageNode; @@ -234,7 +233,6 @@ class TestCoverageTree extends Disposable { instantiationService.createInstance(FileCoverageRenderer, labels), instantiationService.createInstance(DeclarationCoverageRenderer), instantiationService.createInstance(BasicRenderer), - instantiationService.createInstance(PerTestCoverageSwitcherRenderer), ], { expandOnlyOnTwistieClick: true, @@ -321,11 +319,7 @@ class TestCoverageTree extends Disposable { tree = coverage.filterTreeForTest(showOnlyTest); } - const files: (PerTestCoverageSwitcher | TestCoverageFileNode)[] = []; - if (coverage.perTestCoverageIDs.size) { - files.push(new PerTestCoverageSwitcher(showOnlyTest ? coverage.result.getTestById(showOnlyTest.toString()) : undefined)); - } - + const files: TestCoverageFileNode[] = []; for (let node of tree.nodes) { // when showing initial children, only show from the first file or tee while (!(node.value instanceof FileCoverage) && node.children?.size === 1) { @@ -334,15 +328,7 @@ class TestCoverageTree extends Disposable { files.push(node); } - const toChild = (value: TestCoverageFileNode | PerTestCoverageSwitcher): ICompressedTreeElement => { - if (value instanceof PerTestCoverageSwitcher) { - return { - element: value, - incompressible: true, - collapsible: false, - }; - } - + const toChild = (value: TestCoverageFileNode): ICompressedTreeElement => { const isFile = !value.children?.size; return { element: value, @@ -410,10 +396,6 @@ class TestCoverageTree extends Disposable { class TestCoverageTreeListDelegate implements IListVirtualDelegate { getHeight(element: CoverageTreeElement): number { - if (element instanceof PerTestCoverageSwitcher) { - return PerTestCoverageSwitcherRenderer.height; - } - return 22; } @@ -427,9 +409,6 @@ class TestCoverageTreeListDelegate implements IListVirtualDelegate basenameOrAuthority((e as TestCoverageFileNode).value!.uri)) : basenameOrAuthority(file.uri); - templateData.elementsDisposables.add(autorun(reader => { - stat.value?.didChange.read(reader); - templateData.bars.setCoverageInfo(file); - })); + if (file instanceof BypassedFileCoverage) { + templateData.bars.setCoverageInfo(undefined); + } else { + templateData.elementsDisposables.add(autorun(reader => { + stat.value?.didChange.read(reader); + templateData.bars.setCoverageInfo(file); + })); + + templateData.bars.setCoverageInfo(file); + } - templateData.bars.setCoverageInfo(file); templateData.label.setResource({ resource: file.uri, name }, { fileKind: stat.children?.size ? FileKind.FOLDER : FileKind.FILE, matches: createMatches(filterData), @@ -621,59 +605,6 @@ class BasicRenderer implements ICompressibleTreeRenderer { - public static readonly ID = 'S'; - public static readonly height = 28; - public readonly templateId = PerTestCoverageSwitcherRenderer.ID; - - constructor(@ICommandService private readonly commandService: ICommandService) { } - - renderCompressedElements(node: ITreeNode, FuzzyScore>, _index: number, data: PerTestCoverageSwitcherRendererTemplateData): void { - this.renderInner(node.element.elements[node.element.elements.length - 1], data); - } - - renderTemplate(container: HTMLElement): PerTestCoverageSwitcherRendererTemplateData { - const el = document.createElement('div'); - const text = document.createElement('span'); - el.classList.add('test-coverage-tree-per-test-switcher'); - el.appendChild(text); - container.appendChild(el); - - return { - container: el, - text, - elementDisposables: new DisposableStore(), - }; - } - - renderElement(node: ITreeNode, index: number, data: PerTestCoverageSwitcherRendererTemplateData): void { - this.renderInner(node.element, data); - } - - disposeTemplate(data: PerTestCoverageSwitcherRendererTemplateData): void { - data.elementDisposables.dispose(); - data.container.parentElement?.removeChild(data.container); - } - - private renderInner(element: PerTestCoverageSwitcher, { container, text, elementDisposables }: PerTestCoverageSwitcherRendererTemplateData) { - elementDisposables.clear(); - text.innerText = element.currentFilter - ? coverUtils.labels.showingFilterFor(element.currentFilter.label) - : localize('testing.filterCovToTest', 'Show coverage for test...'); - elementDisposables.add(dom.addStandardDisposableListener(container, 'click', evt => { - this.commandService.executeCommand(TestCommandId.CoverageFilterToTest, element.currentFilter?.extId); - evt.preventDefault(); - })); - } -} - class TestCoverageIdentityProvider implements IIdentityProvider { public getId(element: CoverageTreeElement) { return isFileCoverage(element) @@ -687,9 +618,20 @@ registerAction2(class TestCoverageChangePerTestFilterAction extends Action2 { super({ id: TestCommandId.CoverageFilterToTest, category: Categories.Test, - title: localize2('testing.changeCoverageFilter', 'Filter Coverage by Test...'), - precondition: TestingContextKeys.hasPerTestCoverage, - f1: true, + title: localize2('testing.changeCoverageFilter', 'Filter Coverage by Test'), + icon: Codicon.filter, + toggled: { + icon: Codicon.filterFilled, + condition: TestingContextKeys.isCoverageFilteredToTest, + }, + menu: [ + { id: MenuId.CommandPalette, when: TestingContextKeys.hasPerTestCoverage }, + { + id: MenuId.ViewTitle, + when: ContextKeyExpr.and(TestingContextKeys.hasPerTestCoverage, ContextKeyExpr.equals('view', Testing.CoverageViewId)), + group: 'navigation', + }, + ] }); } diff --git a/src/vs/workbench/contrib/testing/common/constants.ts b/src/vs/workbench/contrib/testing/common/constants.ts index e879003b6a1..0c098753237 100644 --- a/src/vs/workbench/contrib/testing/common/constants.ts +++ b/src/vs/workbench/contrib/testing/common/constants.ts @@ -66,6 +66,7 @@ export const enum TestCommandId { CoverageClear = 'testing.coverage.close', CoverageCurrentFile = 'testing.coverageCurrentFile', CoverageFilterToTest = 'testing.coverageFilterToTest', + CoverageFilterToTestInEditor = 'testing.coverageFilterToTestInEditor', CoverageLastRun = 'testing.coverageLastRun', CoverageSelectedAction = 'testing.coverageSelected', CoverageToggleToolbar = 'testing.coverageToggleToolbar', diff --git a/src/vs/workbench/contrib/testing/common/testCoverage.ts b/src/vs/workbench/contrib/testing/common/testCoverage.ts index 37387047d93..321434bd602 100644 --- a/src/vs/workbench/contrib/testing/common/testCoverage.ts +++ b/src/vs/workbench/contrib/testing/common/testCoverage.ts @@ -131,20 +131,7 @@ export class TestCoverage { if (chain.length === canonical.length) { node.value = fileData; } else { - node.value ??= new ComputedFileCoverage({ - id: String(incId++), - uri: this.treePathToUri(canonical.slice(0, chain.length)), - statement: { covered: 0, total: 0 }, - }, fileData.fromResult); - - for (const kind of ['statement', 'branch', 'declaration'] as const) { - const count = fileData[kind]; - if (count) { - const cc = (node.value[kind] ??= { covered: 0, total: 0 }); - cc.covered += count.covered; - cc.total += count.total; - } - } + node.value ??= new BypassedFileCoverage(this.treePathToUri(canonical.slice(0, chain.length)), fileData.fromResult); } }); } @@ -236,6 +223,15 @@ export abstract class AbstractFileCoverage { */ export class ComputedFileCoverage extends AbstractFileCoverage { } +/** + * A virtual node that doesn't have any added coverage info. + */ +export class BypassedFileCoverage extends ComputedFileCoverage { + constructor(uri: URI, result: LiveTestResult) { + super({ id: String(incId++), uri, statement: { covered: 0, total: 0 } }, result); + } +} + export class FileCoverage extends AbstractFileCoverage { private _details?: Promise; private resolved?: boolean; diff --git a/src/vs/workbench/contrib/testing/common/testCoverageService.ts b/src/vs/workbench/contrib/testing/common/testCoverageService.ts index 99e86e86a95..e1b62d541dc 100644 --- a/src/vs/workbench/contrib/testing/common/testCoverageService.ts +++ b/src/vs/workbench/contrib/testing/common/testCoverageService.ts @@ -80,6 +80,12 @@ export class TestCoverageService extends Disposable implements ITestCoverageServ reader => !!this.selected.read(reader)?.perTestCoverageIDs.size, )); + this._register(bindContextKey( + TestingContextKeys.isCoverageFilteredToTest, + contextKeyService, + reader => !!this.filterToTest.read(reader), + )); + this._register(resultService.onResultsChanged(evt => { if ('completed' in evt) { const coverage = evt.completed.tasks.find(t => t.coverage.get()); diff --git a/src/vs/workbench/contrib/testing/common/testingContextKeys.ts b/src/vs/workbench/contrib/testing/common/testingContextKeys.ts index 2c3d0b8c79f..96682ebf7c9 100644 --- a/src/vs/workbench/contrib/testing/common/testingContextKeys.ts +++ b/src/vs/workbench/contrib/testing/common/testingContextKeys.ts @@ -23,6 +23,7 @@ export namespace TestingContextKeys { export const activeEditorHasTests = new RawContextKey('testing.activeEditorHasTests', false, { type: 'boolean', description: localize('testing.activeEditorHasTests', 'Indicates whether any tests are present in the current editor') }); export const isTestCoverageOpen = new RawContextKey('testing.isTestCoverageOpen', false, { type: 'boolean', description: localize('testing.isTestCoverageOpen', 'Indicates whether a test coverage report is open') }); export const hasPerTestCoverage = new RawContextKey('testing.hasPerTestCoverage', false, { type: 'boolean', description: localize('testing.hasPerTestCoverage', 'Indicates whether per-test coverage is available') }); + export const isCoverageFilteredToTest = new RawContextKey('testing.isCoverageFilteredToTest', false, { type: 'boolean', description: localize('testing.isCoverageFilteredToTest', 'Indicates whether coverage has been filterd to a single test') }); export const coverageToolbarEnabled = new RawContextKey('testing.coverageToolbarEnabled', true, { type: 'boolean', description: localize('testing.coverageToolbarEnabled', 'Indicates whether the coverage toolbar is enabled') }); export const capabilityToContextKey: { [K in TestRunProfileBitset]: RawContextKey } = { From 167cae71b1b7aba73accb6596158f989f92d2ce1 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Tue, 14 May 2024 12:06:32 -0700 Subject: [PATCH 172/357] Fix reviving IChatAgentData in the request part (#212732) Missing some required props which are not in old persisted data --- .../contrib/chat/common/chatAgents.ts | 23 +++++++++++++++++- .../contrib/chat/common/chatModel.ts | 24 ++----------------- .../contrib/chat/common/chatParserTypes.ts | 10 ++------ 3 files changed, 26 insertions(+), 31 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index 906ec0914b3..e6af8717dc5 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -9,6 +9,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { Iterable } from 'vs/base/common/iterator'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { revive } from 'vs/base/common/marshalling'; import { IObservable } from 'vs/base/common/observable'; import { observableValue } from 'vs/base/common/observableInternal/base'; import { equalsIgnoreCase } from 'vs/base/common/strings'; @@ -23,7 +24,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { asJson, IRequestService } from 'vs/platform/request/common/request'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { CONTEXT_CHAT_ENABLED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; -import { IChatProgressResponseContent, IChatRequestVariableData } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatProgressResponseContent, IChatRequestVariableData, ISerializableChatAgentData } from 'vs/workbench/contrib/chat/common/chatModel'; import { IRawChatCommandContribution, RawChatParticipantLocation } from 'vs/workbench/contrib/chat/common/chatParticipantContribTypes'; import { IChatFollowup, IChatProgress, IChatResponseErrorDetails, IChatTaskDto } from 'vs/workbench/contrib/chat/common/chatService'; @@ -473,3 +474,23 @@ export class ChatAgentNameService implements IChatAgentNameService { export function getFullyQualifiedId(chatAgentData: IChatAgentData): string { return `${chatAgentData.extensionId.value}.${chatAgentData.id}`; } + +export function reviveSerializedAgent(raw: ISerializableChatAgentData): IChatAgentData { + const agent = 'name' in raw ? + raw : + { + ...(raw as any), + name: (raw as any).id, + }; + + // Fill in required fields that may be missing from old data + if (!('extensionPublisherId' in agent)) { + agent.extensionPublisherId = agent.extensionPublisher ?? ''; + } + + if (!('extensionDisplayName' in agent)) { + agent.extensionDisplayName = ''; + } + + return revive(agent); +} diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index a626cfb8731..6fe68717c3e 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -18,7 +18,7 @@ import { IOffsetRange, OffsetRange } from 'vs/editor/common/core/offsetRange'; import { TextEdit } from 'vs/editor/common/languages'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; -import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentRequest, IChatAgentResult, IChatAgentService, reviveSerializedAgent } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatRequestTextPart, IParsedChatRequest, getPromptText, reviveParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatAgentMarkdownContentWithVulnerability, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgress, IChatProgressMessage, IChatResponseProgressFileTreeData, IChatTask, IChatTextEdit, IChatTreeData, IChatUsedContext, IChatWarningMessage, ChatAgentVoteDirection, isIUsedContext } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables'; @@ -667,7 +667,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 - this.reviveSerializedAgent(raw.agent) : undefined; + reviveSerializedAgent(raw.agent) : undefined; // Port entries from old format const result = 'responseErrorDetails' in raw ? @@ -712,26 +712,6 @@ export class ChatModel extends Disposable implements IChatModel { return variableData; } - private reviveSerializedAgent(raw: ISerializableChatAgentData): IChatAgentData { - const agent = 'name' in raw ? - raw : - { - ...(raw as any), - name: (raw as any).id, - }; - - // Fill in required fields that may be missing from old data - if (!('extensionPublisherId' in agent)) { - agent.extensionPublisherId = agent.extensionPublisher ?? ''; - } - - if (!('extensionDisplayName' in agent)) { - agent.extensionDisplayName = ''; - } - - 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)]; diff --git a/src/vs/workbench/contrib/chat/common/chatParserTypes.ts b/src/vs/workbench/contrib/chat/common/chatParserTypes.ts index 6d5cd8c0a39..66bc10c2061 100644 --- a/src/vs/workbench/contrib/chat/common/chatParserTypes.ts +++ b/src/vs/workbench/contrib/chat/common/chatParserTypes.ts @@ -6,7 +6,7 @@ import { revive } from 'vs/base/common/marshalling'; import { IOffsetRange, OffsetRange } from 'vs/editor/common/core/offsetRange'; import { IRange } from 'vs/editor/common/core/range'; -import { IChatAgentCommand, IChatAgentData } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatAgentCommand, IChatAgentData, reviveSerializedAgent } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatSlashData } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables'; @@ -154,13 +154,7 @@ export function reviveParsedChatRequest(serialized: IParsedChatRequest): IParsed ); } 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 - }; - } + agent = reviveSerializedAgent(agent); return new ChatRequestAgentPart( new OffsetRange(part.range.start, part.range.endExclusive), From 67caa89ae7cb4a6cfae869ecb5a7775193f2a59d Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Tue, 14 May 2024 11:59:37 -0700 Subject: [PATCH 173/357] fix: avoid chat input shifting around when attaching context --- src/vs/workbench/contrib/chat/browser/media/chat.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 95935ccc497..6be7e3e6f59 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -486,6 +486,7 @@ .interactive-session .chat-attached-context { padding: 8px 8px 13px; margin-right: -3px; + margin-top: 4px; margin-bottom: -4px; border: 1px solid var(--vscode-input-border, var(--vscode-input-background, transparent)); border-radius: 6px 6px 0px 0px; From f71359312b0b80ae8c4df4d07124408ba81e3af6 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Tue, 14 May 2024 12:00:49 -0700 Subject: [PATCH 174/357] fix: don't show Attach Context for inline/quick chat --- .../contrib/chat/browser/actions/chatContextActions.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index 3ff7d539025..8597a0dc35c 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -12,9 +12,11 @@ import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/act import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { AnythingQuickAccessProviderRunOptions } from 'vs/platform/quickinput/common/quickAccess'; import { IQuickInputService, QuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; import { IChatWidget, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { SelectAndInsertFileAction } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables'; -import { CONTEXT_IN_CHAT_INPUT } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { CONTEXT_CHAT_LOCATION, CONTEXT_IN_CHAT_INPUT } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; export function registerChatContextActions() { @@ -30,6 +32,7 @@ class AttachContextAction extends Action2 { id: AttachContextAction.ID, title: localize2('workbench.action.chat.attachContext.label', "Attach Context"), icon: Codicon.attach, + category: CHAT_CATEGORY, keybinding: { when: CONTEXT_IN_CHAT_INPUT, primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyA, @@ -37,10 +40,12 @@ class AttachContextAction extends Action2 { }, menu: [ { + when: CONTEXT_CHAT_LOCATION.isEqualTo(ChatAgentLocation.Panel), id: MenuId.ChatExecuteSecondary, group: 'group_1', }, { + when: CONTEXT_CHAT_LOCATION.isEqualTo(ChatAgentLocation.Panel), id: MenuId.ChatExecute, group: 'navigation', }, From 987a4c70985fd20017562246930b0325df93248c Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 14 May 2024 12:50:00 -0700 Subject: [PATCH 175/357] return if `activeTerminal` is undefined (#212735) fix #212721 --- src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts index c4d6e65f891..12ed79506ab 100644 --- a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts +++ b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts @@ -435,6 +435,9 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { public terminate(task: Task): Promise { const activeTerminal = this._activeTasks[task.getMapKey()]; + if (!activeTerminal) { + return Promise.resolve({ success: false, task: undefined }); + } const terminal = activeTerminal.terminal; if (!terminal) { return Promise.resolve({ success: false, task: undefined }); From dc51a8c7c7510d1e805e6122c3702dda47788b22 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 14 May 2024 12:52:53 -0700 Subject: [PATCH 176/357] Add sound from sound designer (#212739) --- .../browser/media/success.mp3 | Bin 0 -> 24832 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/vs/platform/accessibilitySignal/browser/media/success.mp3 diff --git a/src/vs/platform/accessibilitySignal/browser/media/success.mp3 b/src/vs/platform/accessibilitySignal/browser/media/success.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..dee1d5061e45740c3bd1ee82ad1407ee4787c5e0 GIT binary patch literal 24832 zcmeIZXH-*N*DkzM3894^kP@nNcj(>FTj(7DLoXr%A_5i?dJ{qq3QAF$sGy)AA_*YU zM4H$Y1Q8S!0Sj1iqR;bwKfd$hjC02G;~nqqVvOvSy^}TPyw=5BbImnlW3B-OSWPm) z&D9kE_yOXfu!ta>Q`{k(7Y+}qX{za(Iobn2I1myY7U6az+QHGq9>9UW8(z!o-yLn8 ze|0o1bpO@S0>OVj0{?vn|L=~v1!(f;k-CMp>Hq#2;E%M_|B~iv?_~OKX)J;N`nWp& z-yIzb?SI|*qYVh^>Hd4@-|zoC(XjllFa6h@|L@WNq4OJ)zwIvqe-ZeLz+VLZBJdZ1 zzX<$A;4cDy5%`P1Uj+Ul@IM6s=JyW5A6s)MV!l^lP(*%$`_ul4)1W8{nhe!mFk%ZtPwU zb`X%_%4Va$>KDwVq+f&*ZAGSv0KI3bfBAuGL8 z(JqQQp)qIgy`nER}vy|B^`q`6(|!KJqvy zNmc>X=rB+VI1e+$Orn7Eucj(um!k@ZCSS-=1I|x$L!FrB!D_Q7HQ)pMF0?o~4Sg62 zVZJ-7pv^pENt2j46{HYxA>#a}^XKnrW^yU(UQieaLX8dWlT=KcdmZH(EvY=@?_qsc z54Aiul~Eir?Z7&KaoN!;+eW_ACUcWzbvbr z*fL2s>+W_8pfV5EY-B>|=F8A|2_&3~96}C5fWEaxQB@6%kQBIl>7YVPZoeUV|XKN+jf35tb9I2(HFD!VTEVyB4w<~ zkMff}hGm$ED?_FMI_#M1A}=-QF)A9t$txJEZcUxBc8lPAywBS;$k}JnHD6A3Br4Lj z;hDCU?pR1L`4P%k7M*IcoO_%QC4I;V8X<5oRj7rsr#F*}qx zUOH6xGs>?ix_8tt!r|Wr`y|(%SS%0TmtqSZ{KW&z2z!9f`BV}a2+nbO6GDML2N~9t zTV$(|Hp>^3I7w>ZhI1iNB9$Wfsq~MxW({O2#=a}$oAK@1KBQdw;C#mn{y1z|2nUD} z_@qY*T=SLn;IRA+hurUnPaos0Dy&bwc!hs<|G=s5npI5ZXXX_qa|a*uG% zq+QtR>US4sg}S~gV;-GmKCOKw&*A`HsO@M$qv;0VZ#VXIb8W(E=5q?qoi}@#OlAcr zWl{^av2E$<83byiqz!_{%|q~=1)x48TNZzcavDrw>;v@}QXohnfxCcAgNjXFlsMvG zncp38RnEhya++L>Q2ma{mI^sx3uwWW}XlLb0R0Hu$>9^P4~30BqgpR@!&IdGg&G)RYikPN!$TcvkFEkrHPaKg(iJ;%rGJ z&!3B^avv_R4d0de>||)IEBM^#c;oAYcQ5-sFCYBuCgAI9DCR7zp?kWBIC1xIr?_*x zsb=BaA02BB4{iznOGoB;x&^$AE{iFBDo0!E*JEpCs2~Oh6a{fJS;UTk18h`!2gH{y z0wL48ptJovu&T`sn4q0j;>0S_D#tTFsImV(^XNYI`{#c7-LIO6W~v==_x`N6WU%^l zBW0DzTz?uOJI)5Chq{UhK(N-Ahf0YF=ZBv?qktk+y}NEjqRx2_7dD(Ryi_IAr603(;NQm1D^ z4I4WSaXs%nF0AB#COh%=^cE!C-*;^F!_$T0sUV9^Sf=sa|#e zXzthF{5e8~Z5r%N`))da@^09AO1Liu`x-5{13-~t0j7Nf;8@|&k4)wwbI@4#+*n@9 z?|;oR(i^lFL0_xXnuwKPDhE~>n82!--9J-jj=<(oEV0@^7#2l8h!rO%W3dEfLCSHG zDp>*waDu?~52r~%selN0wY@}**Cd}SeO1C$9S4oqz{k~UlT4|B!H4fHN7_ePjIGwh z?rwe_J?wsLtwXZmMZ@I5pNJGI=Zm9Gfd}%g21ORS#N^kz1n01YlgKa}sgV0s^cfy< zpaT3GX$C?iNkdvm@sK5wKhSEMBDlSO%ZjnG&Ma4N-Tm4uoD z4(X3)bNZ2W0%^k5q7judK3qB)&Z)M|N-r{qr%O7Iez@mny`re6>{yGk0g z06(E(>T-TWNS5Z?osU@ZGfVUhntUM^e3Q-DW#Y8bL=i?Tcw5(EZ0S{bo~o$LRe@)D zl@;vQ8adv0+?w5(dMtR(2Z;_@wl+2BUXrQrUYVE+ zs!nOR^0V*Pza{C-t<}Iczl`kZYS$cnvRm{`Z1LX zyVYotz8iL(Gv*Jhoeng;+tnP|qqbHOo_teFLEyO;WHK{Q^nrh68vEX9j8&fqv} zI8lr}$}p#0qoiSX*R9hc2eq&=1l4RZSsIF=#{vL-36egajVhmE9O_d%t7ItVzGw7x zqlIDYPI#yCs*jykXvcMlZvWo1Vi8tiPRZ6D2tA^bnFt5Jng4x#qczW~y$6_%rf=1D z*l-~qn9N^kkS3WKc5<&-=x_f+0brjD7_je3^T}c1AZz{N$^HoCS-MEMQC&m%Q5I9+ zUm}dNF~bonKsq&lCE)(JuGK_9vC&7(b!r*7O9 zv+p!$o~OI8mG+|*z(gO)VR0nWw&_BB@q*RJ%E#Db z3A1fx-Tou-zh<3zb`d~#fQDD68-WD*Dtt^TMLi7Rjel?UL;XbmLolE23u=&UL3SXS zm;8&gha4O`G|QJ{7wnkWl+G}@t`cdd^H*pdw9;=C1iI_y&Tc4T1%t%uGpUA_gWt_!&Rdrk@YfFJ%@UJ_OYg>1VLa6 z;u~ihQ0T7<5+?1MkpnGUC5hto$avWxgd1c4`UoBbI3X4RS%e0FLcoNsx7FV-HQ5xw zcqv?ayP>cCsMVp~`ozg=M<@qPu5Mr4jOo%pIUgf|V$e^LXbF6R`M7IRZb`4|7DvS` z+|^GfYi(7#_T%ok==Yre`0IAYOtVm8gz?AT6y^<}&qFT_)`exzs`^Y|EtU|-$yll8D$C^M9k6u+3gtn2O=J6FtL zl$p=$a}7)>w<}^276dhfZvtD$8Q>dRz@*51CHK;C0??0WJJONi)H8A6#F!SBPi6V! z-s`xq+mUj>#1KyLrZWf1mN$LDwe@py&EpN1%201rH9zaYkcK?k8OtoMVKPtfykVX*q$feU z=p=Z0Kg}8Uq(aef;2xg}<8@Z}zWo-<> z9d@R%(D)}=x_wj+eOP}oM{KN<+gxf)HFl}g=S9oUeOkMtysq=9H#O;6$>koI$k|vEd&Ko76>pR${N?3#u-C@c5}tvczoTrL%pWg*FsX;Bne1K<-h`NXvDdiykk)-Fv>XZT*aKJ*Z ze0?`N@}%>YY|IFG6k3@khT*0D>h@;*60gm25w9fU;&{7_!$!zm5)oNy9FJ3tIlIX; z3?$7O8FK(PP&@}@hP*~+bA|?IbHSrG*V=c^$1Pt-IA9tz{`Ah|nfw9ApTmK3&8r_D z4=J}`S`%Oa&`}Vx*u={{C=lF^Z4|d&anXEuv)ZNU!2@32&p-Qqc%1W0!-2Yxy`ZV- zbug&NPh++4GUx~Q27Mv#!EZc{T9;4V0_Rc_z(kV)-Oi`#G!bx}9n=(nL**fGGkRKZ zNZtc`6Tq7S00QhbI{Fs8WjY#U%5^@_T9>04k3Gb>qWOv{yX-*yjI z2AS}=<@=W4P_HFA0lsg}vQJb=C2$xek zF!c8Q8Ov_;MSV&^iHWqX&}(xd(dB)ru4*OHU=8s8(G)w7vh# z>cTh=U7jdTHQ+p8QeW^r;1Z`hfoEJY2FTwH@^Y#)V)OL~`^Saa+{Xj;mLDi$ax=J7Mql?z_(OIO#6rfvgQkLbHxnH=&>`g@h0!Fq$E2XH*>M|T`X%5bSH*W)a&DI_ z>i2#c4&-jg9pKw;{c__sOMS*M|bgVfaC@B?0|Z z@q&U71&T#!rW8W#X-im=GIOerS=7fADVtphXLuqA{UE#SP^~86Q-IE`J7bU(5q7B+r^%zgR;J<7ZAHY<=KlVa$*++K}PR?si8R z7EBidreSh`n#hrXA@L3r6=rnp&;oS6C{3zteC~{ItV(QS_U(^@R|D0jL(10AoGb8Q z&Qd9UVtA$3LMD6)#r2gQ$dV#-{>6&hFHZL@SvoR*?CqQC+RgkakM2^mxe3;xeAZkI;#)~bO_XZ`2bQ?3XuV4w0;Ww>0xBW6v z!ti_%&XcZ>JMSv>j3(xNgnH5PJ1Rqt35P9Ke0^GMDmY9UOw+TVn&--8FT3X@UlkPX zkw0;E_T`SZ0rQvS%av#R%wJTHhkgh2O)bC^J;L-pP88$!4w!%`lxWb6oCOBTq17Mu zUjkiB-{Tz#?jVXRp!P&Sp*{EdSpa61NsJNEmHR+;YpXqSyV>zTw)g%U%?oxP?u?vq zJb&}y?)j#CF1^0gG!(`Z#Zw?<@~j|xEyJq!frCpgD)*psh3DaviJOO*eYt(&pQGhT zSV{xw8iE*@ed)vDsIx#IDdR0^8!|{Lf@+gK*~*5z%_tx>X7UBrkw-~9fWp3wAM&4a zJI`WZDZF@k@cgNp5SRXpxt++~9)+TFVTbx@zV8vLjiy?Wm%gbRz#PIJfdUjZ9OILV zfcQwTgsHlo#Alc0^U{i)rk5{YVd}5;F+X`O-o7+A&N**4&g*nJ-*8F&o-g!Oz9hMN z{LlJT3c#(1{jB(TJtu-jND1-h<<;V5pRM`_`Y`OGBGUHv$Bta_*&Y87`^_rzAI^+(rz z6P`Lgp{~-^VwSifpQe}x`Z*$euLC1GFTAex{V?MGli!h*C19`rnVeHfpJh2}DfG&~ z50kBj5^C6f9J-YL6yZgG3ZV@SCMb+Bfa7#gR_5ppi_99pH#*V=NQcRL7Qnq3BhA;Ru)Wvc4*=efa=$@ygDth4Z%G&ioQAq;zfW{mgvS_j`r0Ars&%y_IhUdP;2zLBR&^5#d_85?^aRpn#g%$rJ(+Wr$aKf z>gLYV^&uw(L5yy;7~yTuo#4AK6rAq-d_o2-82Mldl!-5#Q8#e1)x!()7lVe78oXeC z5T4f(ulW*~0zF`KkZ7U}3IY*2Vflozn7~&#&l(!7!$rj-Eh`hZrAg}DVhG@tRM5LG zt}f&L4-8&BzBGEZzc})&_sz=3LfL7&!P~DzErOmWxEXF->3x1GDa+yGpR~yPP=m?2 z_%hb+3?7WFN8$FkiexCKb(dC`@cK%@kR%BijG^jb?$)RoB8Eo;k*J>rpXFPGcp+V* zWA3o~WU6_c9Ip?rX6sfZ&hC*B%LDDc%2t+U^IY*d^}+G}WdGw!U9WZIPB)excKd8c zh1HED{;1cNkqD|bo7_;p;_0bJTMllVS=rpfoO#N;!P3S2w=U2scC+8>*C_x_v^PuJ zaaqr2Z;DB=^~b0xV|Z3B_M2R&rLg=DbP{@)i!;x))z>2k&550X)yNLOR&WCl-GLHt zUF0x~D4zzUA<`f#u*33+v_%8iX%V4)fJW%}O8Z6~Wz~6?Z^_-{vzJe_hJ}4EVb3~x z>g2+s>a&X;bZSkFI?*MIwyaC-VzdmPQyV>!_trxJD;N(L$!H;|!>a;Nh$a%7BzIUV>Wfagz0t+rh)+``#~{) z4$3gzftGov@GX=)P=$;FO~`C0fI3<$gt^HOc$y#Jecn+y)t!@pxFy70#p~3nJMOULGk-{Z(mqe$1N@*T zu&oTbhitvs$B$zSAUYUTa9z4N`#8f*v+8aal1P6B)Clkoli-<0tYb6SK%q&Pg3aBV z#Fbj7Q#Eak{@x$eVqDdex?Mlrh+VC}RK2EkPUD73d!h*tAjB_#RkB2JD^t_?azuxO zg1CYMchwzaNoP4^vsY0hWynbqI_((=x|cihS-~JlF7-EmvH;i%+d&vB{z*~vib=8X z=P^>|X61kS(H8<(`JY4&k`lm4awq$ck_C_f80aXeikwc$1d_!dujF+QX9GOM#RATn zeNSC9)fSPxc~AACdS?D+h@;Or&nSfbHA0+XEX87^EkfPwvTd9oX znZMYtB8OxA5KM$z9=BcB9_Ms2CGr3QM4$~$IdkIl#v?8P2g*YEShX|dl)M~&@%;sR z$s`%-yR5yA_AuY_J1u)XF22>;d-R(uxlu?E3m4!wmC1q+q61!;D;217tKOGxp4dQ_ zu9|vXJXn;{^7!`48(FtyA22`DB*^hJZPFsPvoPn(8_IcX0zscDyOPdQ6QH>v3KjC;7oxAUh}jrcAp#5E|KEIzH{ zb0>oMP(BN@@LG2$V2O+0rFtb`(48L>`pZX8?x2#B55#??$ zw*XKU*eitY8l2ZzTTtNP>?cB~bPb3AeUWt%qRdlzhaIS)3&7Hsip)*y7S6=D@m|ll zW%ub_>AX9_$@_`5XmYuMc#l1&L%F0^vetd4?t(ZrbfUhR6XOo`@bpyFK5)RFPjOVS z&IaxQ@+LOb`Q)00m0G;&xIX3aRXo_1qy^}ca>*`%?80gX%h@&plz}QzBsrTT#|or1 z;naXmiY7?}*dg%((Ihw-B@A5}g{|9G92{4}`)6Ry+b$_q$&W5E5BbV3-2T4bC&|#W zT`sN=&C!s}UPE1<QKCa_haL}`jlTXBPw(xFi$Up zb@bN=VrP$ITEo=`ptn%2Z~%4<2$n73{mmaL0Q;)QV4Z)F$SdCnCtmw=4>qHCUMr@m zULa9^NF~NY4+DlAGu!T@q?H$UQf-F^Eq;`GQRP%_JC7x-e6Wk{+#8Q^zHpx^^T6r} zsYgW~_sI3DP+X3da@U&z+hZPQkG)Nl7Q%-5odRRh?&g!0^_eePZEazA3Llz1BSuN8 zrBrPsEgv&QJ_`lYxds4TLOn_a9R%pYF!SgEve0qwt-98_l zOH2H!uFJfw`Qkv|+uO{KzxICWeX&!yLk6u-3#cKQ)v<*ai%6O|2c>|V#|o(yil~aB zAJeDE1Tu{qiWPuT&DgFxrwTIKtmjcD6E<&lJ*VMU@`he<4ms*6WE;jRiTK{p+E?pb zoQx^DXk>h%@lx)kea4w79;Z9w?&+SrW-as1epf>2_t)A5=Af^9O#dmBdJZ08+#6L_ zq}cW06z5c|29_JOop$S#sEU*xIWY!no}NR-h+q>d7zG%AEFX0K0AYTm_C_c7*xW6@ zv9i(n@X-my@aIp86p(xE_mrB!TH$i|tHNaWyEb=uIm!fuBURh{;jYf=*1d`DTJjGr zuLyK59I(=Gtrh!@@JF>0W+?PeTEkU_-wYNUydOW@ip%97GYA6X z2x|D$l3G4AHJ=lj39CWD0HLCJf5h966TkUW4ZtlxLfWIi`4qV>+%ZKW@6s5+W0aR_ zer_J}raeY>>8FQ)xyYWdOD$zVIcN_P5z7|xLgtuuX@_8cvrn)_6|?ShBhorSFA}v( zZ4D9E{a91b8S=SnZa?k&{*d@N!$1oN7zKB$gk}R|MzSg)?!;J=CWrdRrax)D z4G&btmv^cikOeV0)9MNl&eAGQ_e4^yoGbi%Rc>}HACQq%Nz%JsefZMz2eMZa!kfHo zxjFK9;d4T^V;&m{wA}G4W=)O6ijSjf=9C%%5rk-k}(YtYd;+2-5|Qvn+zeN zN#kgw#_rJsZI2-&y)NK|`Lawvn+nImovPpbsQ}=1cslI9?R?gd0ty=Kq86ObHwa8VE4wA06a0Q@YRr0M`p zWeF(6)c#jXK&LhluZ z>3WF%bm*j3IQUh<4G{V(eQA6($E9~g=fx&l*Kbo?+t+D02|>e;_XqEe{L$yX4loEJ6ECAgKS=-g)F7+u(I zPz>Urp9o~oBz{I@!yhDxx(eX;fFLJ7HdPv>3bqN=)*te73&k;Bcj|pjMc170OGry! ze0Zdb9^E5fV1t{|?8w|FkQyHBvR#=sxEWB?pAoUki|Km%qgtGjaMj%e?o$~(lHc<$ ze_RfX3;9BToB@3>aYje@jrY#KJC?u?J85-dAFJkp~2*uNc?1 zuYWSB1a6HZXkr&fHZGq2w$Siso9EDn$zSU?3SY(<#$I?>I4Md8ML{q;#)$o$k+8zC zSK9}_!=EviyGYhBc?78`!0DcR4}CZ&{x^RL$Z#FCy)6F|n2GqhevC+TS{r5YCyz2K zLV~PG#Q>oyQrZEg0+wvhM*$5;OX$2QhI5>q6GTtlj?-*c-sPOTkyz`ixQ~D20k779 zgTabk{;=m9NP1+Ge3AZhfC?>t8w27*H?J8~IR=dR*fDM(Nz{Rj$6qL)B_4 zO0pdM=v&&yOH!lF&(ysU)@s-4*)avuvpMr|slkuEWXAV01O`F*as5HkfY<(UEWJ8A z6PnK9eV06%p87S}lebgonrtWcahMHnHRbi}%+&U+&=A*0UP8efuS++BCFEsNGbP?O zVh-}n*y@d>F8Rw9)hvwck-2Ci@urOb3~y0d6)v4AapN@s#6dJ*ZSr`uiEA3!1uPY* zXAeMuc&-xkr!M>@#5D+A7?D1`OWE+UuCJs>hN19>V z?ANz1@7>I6y5#q>FRF*hv}Bct=yNylAXN^QN-Psn#R5)CEj!8hkd_ZE3(phKa2Nuo z5GBWFtq&=a$fQmrKZlI+iO8tiCmdENv)u}~Al9eVmwz#%#I-ZyRqCgR+Yh!ZfArR? z`k^1S{JbyFHZAkoNMtaE@L<;F`Hhg=xz5{d1qaJ-hg|79G*S>Ia~1JKX`ZXuW;z6J z@j&Q7cNq-^;K{?gY3@*bFL{#Il>I5U?J)L_{~^QeRQq6$0|N{1R!kOPLCjyT10p#0b~M_43GpM%|)|1z!M^G3!hgwj%Vo_>X)HFmZ(YVJ@bH~ zO448wR62?AN#(YhmyHDQx$v~Qj()1#*^L!xyMzKA)O1Y>EtI z)Nw+-EXlLCans@h^|@h5Ug;GKZG%d|H@dX5*)H&OKC1)i=4lMm7J+UT$h@pmIyXEI zU6M4Ex|&U`A=LJFq~Cm3Tr+rWR5kdZ&&}t|p<0Lavj(BC<#H$*F_DYR6DOh{TjlQ? zdenPDlz>jPL~u9Ab!BcDzY$Tfd^C*QZfL;VlkX@TVuuBpTzzlAeYn>jl{RJWC>C_7 zyXu`H%%bZ!|D6Wy%?p-^ZeuAD!=Dv?ATY9OSpG${daOl{g3~VTOw2pAJ6o>qmtXk$ zor^sA8r!bl)oCcBo@vOxSJ3@E_kPn$FKjAr2o`7rZXq^;o(eKFhM#D4kW?5pt$$@%;2wJR24EAM>2 zR%HB1UqqNA?NyL_ro4sgh4qewerx4Zbzi3U_ZU-5M>^9z8(XG2!YKj?js`^_KL`PVM>+psIPuX2_>=`8*@J(~T( z-K$KQDSc`391nVTDYQQ$nAF>TszSxy2#82sRHT@6wV_p~L>$-S)k@*p+-@`qLnT;u z%dufb`t-9TaRtro#6WM4>M3c8v|t(fv}Dfpl_w`|v-NL64HvWAcwx z9`+R24Sdh=47KGD5DR(5Xv_YpOH(RWyrT8kb0-`LESR8%Vlh+K!F!XM_e`EGrRg%N zuvG#8*~%mvtAS6(g&i!QyMEY8PF}P;=uvvR*i$XD`GUZ7_Hn(Iw9g}fUtOQ9tM1M4 z4ac4cs^~uWGvS79wBh^c2dm0`bt+N(Z~_93LczufJ}T4jLnUFgvf*=w%>lr5uU^$C zp1z@Z3DD+j*0>Oia(Gjh~e#ii;*k|!d zw`Z@tz!cyC9hjEiUp#h-HZ$58#?V@%0ybM zpRvb_9skfA(x4J;5-9a(zh=U7OkO3Q*et&4(DcuL%3+CguU;WC2y(!JF3dFUpx0dAK9L@ZB*l+@fq>nOA`nu#kIW2UfJIM-P z_Br^SUGk$S;p{FkOLeDtO&0U2=a~A|>7S0;*Y)MX{y{EiJOq$58L;$^~kxq zr9zPN-h^0+)6Bgt&FIcvcl5^@yS`QccAk}aEpBd5UEU&L;GrA=Ujnd_2xm|2G;Vs+ z5gLU2N;VJXL-xCqp&i&IEf74YJvDB%@F+z{xOwsS`32!L8TMMI7dCD>pDY(vbCi?F zTQ$1=d;K!Rp8dW58bODfac8)ES6_`kY913jeB7v{_@c4XlkC~nvWurr94_2^(yiin zb5Zr}+rF*SAENJ`I_BuV?o1%KwQ>`Moh^~%_pwjj8rnY%H@LrZJlhL84xxC_w_J!B zdEnrWm~r>i^LM_95;dKUkA`y+5hip-6FUr;btvZ{b3uf5TFjoqkFfKtnjXE(|E7I9 zhF`E0_ch9_Snk>N3scLh^SPQO>_~ed&L^>=B^J|*D;I(gd8X~XMlNRrpGMqw?f6pi z@^WAKUCkC-UL`kpmITTWSL$A~Xur%m7hxVj0L37i2qzr)#q?SsV2nSBlM#Y?B zu#jLa9?mpaO)p4*tdkvJ=3eC}K&dp!V&%D+$lVL<>UrPqY?-oSj#Oov44spFzt6$O zLN{25568(bB%daOZo)~aGjxNio8DEL1{U9|;x7T+1$EjVM@4@29VhX^Q(rk%N)Nh8 z$oH!u_>w;N`Mr#}ah_H50mtc*$YO6G0G%|c*dQ~H3*b{+-osHB=*6`Rtx-Xv1vxXW zU#XSB4p0pw)#6`QGV2kG^(t6^OW|CoOeHQPAEgN z$5X^v;UYrhq`q3S-O&@4-imkDU7v5ax@V=wt!cjL?j}4?`Rrs13_)?J0Z}5z23Rqv zZnvfA{4-LVilxXBA;eO^5+ILiQ{ObEoxI_ZTr(m0n?J1jp9FkQ8f*VWk*^>hWB29h zxcz}AAScyC#LtAqpYz9?6(zN;zYs@X3s4RElwRNXQI$+B*NG14j4RhDq%PzloiEY!wQ>@+;ByiSv3?5MSxwwDxir0Wl4LuX!*h(TDAr7g`}R&)m(WSc&!F zBjWkT%;~pkTjj2UN+Z5Y7QV-=&a(%`&Jk;q1uK@~dP5XK#GJJx@W)y!XdOc3O3a6N zn%pA9?v9uw&JAfqgf5BKtP$I_E)TD<0@YSH1+M(70uxFY&Tf& zSawFZ`eZGh=G`pS5mTaSGw2zUA2@NS{H~%=*OzeU4YCQT_LB@mw&gV9eAI2d{gWgb zDFCmI7~*`EOB@p9-iyuW$8#Nbvwx#VRFx-&@{Y3u6pLU7(WARLpFNAr+!_tP^oXn4 zPR2gu+mV_b@1T1wseU};&ZEtWx~=RAfHei#jIcnPT<68KD@;eUJKUA;sXOD1U;dig;^$S~-6((n-uZ8RgT0(J8NQr<6Fb>Hc;+PGjz|yxFtv2IF$hI|#6>y+eRI{-c1YGVB&S~3)_Afh*QMgROWBa# zTTiUV9aJYeVrIHu)=@#B{;0y2p%mAzC#ffF;JPnVOyi{)oZ67*B9rCg>U*{t-W>4x zEPDf5n6iWYef(b!k3s}G#d;d4;1(yXTQ&Aws^F(ESqGeHd?m|8aOFO znI64lBFx%9{m*Z|{ZAFEQl4EWO^;rqRC|3rOhz8SKwki40AK^o&}064W8gGA`QKZ? z|KZyG<_{eJ;J^(s4gerHp>=^j7QguY3M22Mk@_se9V zb@(5{|L_L@cG3SCF#NUh{}F-zAAf*9zkg?q^_M^YkAVKezV??t|F8-F51ad!KmTDs z|6#-Z< literal 0 HcmV?d00001 From 51d72a2cb46c404e68a056703ed68a0056f8194f Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Tue, 14 May 2024 14:35:48 -0700 Subject: [PATCH 177/357] fix: layout chat input after accounting for attached context widget (#212741) --- src/vs/workbench/contrib/chat/browser/chatInputPart.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index e295612754a..e2fd33951eb 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -474,6 +474,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private previousInputEditorDimension: IDimension | undefined; private _layout(height: number, width: number, allowRecurse = true): void { + this.initAttachedContext(this.attachedContextContainer); const data = this.getLayoutData(); @@ -491,8 +492,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.previousInputEditorDimension = newDimension; } - this.initAttachedContext(this.attachedContextContainer); - if (allowRecurse && initialEditorScrollWidth < 10) { // This is probably the initial layout. Now that the editor is layed out with its correct width, it should report the correct contentHeight return this._layout(height, width, false); From e3c1bbc90dffbb77bdc2d99fce569c7a8bb5b65b Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 14 May 2024 15:09:18 -0700 Subject: [PATCH 178/357] fix accessibility signal TOC (#212743) fix #212627 --- src/vs/workbench/contrib/preferences/browser/settingsLayout.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts b/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts index 72f128ed81f..a47b4ff6e15 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts @@ -149,7 +149,7 @@ export const tocData: ITOCEntry = { { id: 'features/accessibilitySignals', label: localize('accessibility.signals', 'Accessibility Signals'), - settings: ['accessibility.signals.*', 'accessibility.signalOptions.*'] + settings: ['accessibility.signal*'] }, { id: 'features/accessibility', From 4d03498fcd3b9dc00f7acd79bed3832612852066 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 14 May 2024 15:34:17 -0700 Subject: [PATCH 179/357] on blur of terminal chat widget, if terminal is not visible, hide it (#212747) fix #212672 --- src/vs/workbench/contrib/terminal/browser/terminal.ts | 5 +++++ .../workbench/contrib/terminal/browser/terminalInstance.ts | 1 + .../terminalContrib/chat/browser/terminalChatWidget.ts | 5 +++++ 3 files changed, 11 insertions(+) diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 1dcaecd333a..08162574c71 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -639,6 +639,11 @@ export interface ITerminalInstance extends IBaseTerminalInstance { */ readonly isDisposed: boolean; + /** + * Whether this terminal is visible. + */ + readonly isVisible: boolean; + /** * Whether the terminal's pty is hosted on a remote. */ diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index e6ca453010d..5a7ff030371 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -266,6 +266,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { get isRemote(): boolean { return this._processManager.remoteAuthority !== undefined; } get remoteAuthority(): string | undefined { return this._processManager.remoteAuthority; } get hasFocus(): boolean { return dom.isAncestorOfActiveElement(this._wrapperElement); } + get isVisible(): boolean { return this._isVisible; } get title(): string { return this._title; } get titleSource(): TitleEventSource { return this._titleSource; } get icon(): TerminalIcon | undefined { return this._getIcon(); } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 2c337e076f9..88583834d80 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -91,6 +91,11 @@ export class TerminalChatWidget extends Disposable { this._container.appendChild(this._inlineChatWidget.domNode); this._focusTracker = this._register(trackFocus(this._container)); + this._register(this._focusTracker.onDidBlur(() => { + if (!this._instance.isVisible) { + this.hide(); + } + })); this.hide(); } From 6ffbf8bff214dd5ec98e541b27559e70f75bfc24 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Tue, 14 May 2024 16:07:51 -0700 Subject: [PATCH 180/357] Fix missing implicit variables for inline chat participant (#212748) --- .../chat/browser/actions/chatTitleActions.ts | 4 ++-- .../workbench/contrib/chat/browser/chatWidget.ts | 2 +- .../workbench/contrib/chat/common/chatService.ts | 1 - .../contrib/chat/common/chatServiceImpl.ts | 15 ++++++++------- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts index 784bdcacf66..c91cf01aefd 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts @@ -297,7 +297,7 @@ export function registerChatTitleActions() { } const request = chatService.getSession(item.sessionId)?.getRequests().find(candidate => candidate.id === item.requestId); if (request) { - await chatService.resendRequest(request, { noCommandDetection: false, attempt: request.attempt + 1, location: widget.location, implicitVariablesEnabled: true }); + await chatService.resendRequest(request, { noCommandDetection: false, attempt: request.attempt + 1, location: widget.location }); } } }); @@ -333,7 +333,7 @@ export function registerChatTitleActions() { } const request = chatService.getSession(item.sessionId)?.getRequests().find(candidate => candidate.id === item.requestId); if (request) { - await chatService.resendRequest(request, { noCommandDetection: true, attempt: request.attempt, location: widget.location, implicitVariablesEnabled: true }); + await chatService.resendRequest(request, { noCommandDetection: true, attempt: request.attempt, location: widget.location }); } } }); diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index a3c600ca145..6fc4e35aed7 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -709,7 +709,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, { implicitVariablesEnabled: false, location: this.location, parserContext: { selectedAgent: this._lastSelectedAgent }, attachedContext: [...this.inputPart.attachedContext.values()] }); + const result = await this.chatService.sendRequest(this.viewModel.sessionId, input, { location: this.location, parserContext: { selectedAgent: this._lastSelectedAgent }, attachedContext: [...this.inputPart.attachedContext.values()] }); this.inputPart.attachedContext.clear(); if (result) { diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index c9773981e4f..f83ae8990a0 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -299,7 +299,6 @@ export interface IChatSendRequestData extends IChatSendRequestResponseState { } export interface IChatSendRequestOptions { - implicitVariablesEnabled?: boolean; location?: ChatAgentLocation; parserContext?: IChatParserContext; attempt?: number; diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index 59868a8097f..46f5708a031 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -425,13 +425,11 @@ export class ChatService extends Disposable implements IChatService { const location = options?.location ?? model.initialLocation; const attempt = options?.attempt ?? 0; const enableCommandDetection = !options?.noCommandDetection; - const implicitVariablesEnabled = options?.implicitVariablesEnabled ?? false; - const defaultAgent = this.chatAgentService.getDefaultAgent(location)!; this.removeRequest(model.sessionId, request.id); - await this._sendRequestAsync(model, model.sessionId, request.message, attempt, enableCommandDetection, implicitVariablesEnabled, defaultAgent, location, options); + await this._sendRequestAsync(model, model.sessionId, request.message, attempt, enableCommandDetection, defaultAgent, location, options); } async sendRequest(sessionId: string, request: string, options?: IChatSendRequestOptions): Promise { @@ -456,7 +454,6 @@ export class ChatService extends Disposable implements IChatService { const location = options?.location ?? model.initialLocation; const attempt = options?.attempt ?? 0; - const implicitVariablesEnabled = options?.implicitVariablesEnabled ?? false; const defaultAgent = this.chatAgentService.getDefaultAgent(location)!; const parsedRequest = this.parseChatRequest(sessionId, request, location, options); @@ -465,7 +462,7 @@ export class ChatService extends Disposable implements IChatService { // This method is only returning whether the request was accepted - don't block on the actual request return { - ...this._sendRequestAsync(model, sessionId, parsedRequest, attempt, !options?.noCommandDetection, implicitVariablesEnabled, defaultAgent, location, options), + ...this._sendRequestAsync(model, sessionId, parsedRequest, attempt, !options?.noCommandDetection, defaultAgent, location, options), agent, slashCommand: agentSlashCommandPart?.command, }; @@ -495,7 +492,7 @@ export class ChatService extends Disposable implements IChatService { return newTokenSource.token; } - private _sendRequestAsync(model: ChatModel, sessionId: string, parsedRequest: IParsedChatRequest, attempt: number, enableCommandDetection: boolean, implicitVariablesEnabled: boolean, defaultAgent: IChatAgent, location: ChatAgentLocation, options?: IChatSendRequestOptions): IChatSendRequestResponseState { + private _sendRequestAsync(model: ChatModel, sessionId: string, parsedRequest: IParsedChatRequest, attempt: number, enableCommandDetection: boolean, defaultAgent: IChatAgent, location: ChatAgentLocation, options?: IChatSendRequestOptions): IChatSendRequestResponseState { const followupsCancelToken = this.refreshFollowupsCancellationToken(sessionId); let request: ChatRequestModel; const agentPart = 'kind' in parsedRequest ? undefined : parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart); @@ -572,6 +569,9 @@ export class ChatService extends Disposable implements IChatService { const promptTextResult = getPromptText(request.message); const updatedVariableData = updateRanges(variableData, promptTextResult.diff); // TODO bit of a hack + + // TODO- should figure out how to get rid of implicit variables for inline chat + const implicitVariablesEnabled = location === ChatAgentLocation.Editor; if (implicitVariablesEnabled) { const implicitVariables = agent.defaultImplicitVariables; if (implicitVariables) { @@ -595,7 +595,8 @@ export class ChatService extends Disposable implements IChatService { enableCommandDetection, attempt, location, - ...options + acceptedConfirmationData: options?.acceptedConfirmationData, + rejectedConfirmationData: options?.rejectedConfirmationData, }; const agentResult = await this.chatAgentService.invokeAgent(agent.id, requestProps, progressCallback, history, token); From 7415be2f47e65b1de70d158e7292d245effbb3cc Mon Sep 17 00:00:00 2001 From: Aaron Munger Date: Tue, 14 May 2024 16:18:58 -0700 Subject: [PATCH 181/357] fix config ID (#212751) --- .../contrib/interactive/browser/interactive.contribution.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts index bc1ae32938e..cc21b0371a6 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts @@ -814,7 +814,7 @@ Registry.as(ConfigurationExtensions.Configuration).regis default: false, markdownDescription: localize('interactiveWindow.promptToSaveOnClose', "Prompt to save the interactive window when it is closed. Only new interactive windows will be affected by this setting change.") }, - ['executeWithShiftEnter']: { + ['interactiveWindow.executeWithShiftEnter']: { type: 'boolean', default: true, markdownDescription: localize('interactiveWindow.executeWithShiftEnter', "Execute the interactive window (REPL) input box with shift+enter, so that enter can be used to create a newline.") From 0d641702590f39f08c8a9bdd896adf3b53ba9efd Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Tue, 14 May 2024 17:47:55 -0700 Subject: [PATCH 182/357] Chat participant API polishing (#212757) API polishing --- .../api/common/extHostChatAgents2.ts | 11 +- .../api/common/extHostTypeConverters.ts | 7 +- src/vs/workbench/api/common/extHostTypes.ts | 6 +- .../vscode.proposed.chatParticipant.d.ts | 167 +++++++++++++----- ...ode.proposed.chatParticipantAdditions.d.ts | 25 ++- 5 files changed, 152 insertions(+), 64 deletions(-) diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 8e00dbbccd0..86518eac57a 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -22,7 +22,7 @@ import { CommandsConverter, ExtHostCommands } from 'vs/workbench/api/common/extH import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; import { ChatAgentLocation, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { IChatContentReference, IChatFollowup, IChatUserActionEvent, ChatAgentVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatContentReference, IChatFollowup, IChatUserActionEvent, ChatAgentVoteDirection, IChatResponseErrorDetails } from 'vs/workbench/contrib/chat/common/chatService'; import { checkProposedApiEnabled, isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import { Dto } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import type * as vscode from 'vscode'; @@ -318,7 +318,14 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS return { errorDetails: { message: msg }, timings: stream.timings }; } } - return { errorDetails: result?.errorDetails, timings: stream.timings, metadata: result?.metadata }; + let errorDetails: IChatResponseErrorDetails | undefined; + if (result?.errorDetails) { + errorDetails = { + ...result.errorDetails, + responseIsIncomplete: true + }; + } + return { errorDetails, timings: stream.timings, metadata: result?.metadata } satisfies IChatAgentResult; }), token); } catch (e) { this._logService.error(e, agent.extension); diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index e4031e0b167..dcfbaba32ef 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -2351,10 +2351,13 @@ export namespace ChatResponseFilesPart { export namespace ChatResponseAnchorPart { export function from(part: vscode.ChatResponseAnchorPart): Dto { + // Work around type-narrowing confusion between vscode.Uri and URI + const isUri = (thing: unknown): thing is vscode.Uri => URI.isUri(thing); + return { kind: 'inlineReference', name: part.title, - inlineReference: !URI.isUri(part.value) ? Location.from(part.value) : part.value + inlineReference: isUri(part.value) ? part.value : Location.from(part.value) }; } @@ -2566,7 +2569,7 @@ export namespace ChatLocation { } export namespace ChatAgentValueReference { - export function to(variable: IChatRequestVariableEntry): vscode.ChatValueReference { + export function to(variable: IChatRequestVariableEntry): vscode.ChatPromptReference { const value = variable.value; if (!value) { throw new Error('Invalid value reference'); diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index b69486af399..8ba1eae8425 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -4361,9 +4361,9 @@ export class ChatResponseFileTreePart { } export class ChatResponseAnchorPart { - value: vscode.Uri | vscode.Location | vscode.SymbolInformation; + value: vscode.Uri | vscode.Location; title?: string; - constructor(value: vscode.Uri | vscode.Location | vscode.SymbolInformation, title?: string) { + constructor(value: vscode.Uri | vscode.Location, title?: string) { this.value = value; this.title = title; } @@ -4425,7 +4425,7 @@ export class ChatRequestTurn implements vscode.ChatRequestTurn { constructor( readonly prompt: string, readonly command: string | undefined, - readonly references: vscode.ChatValueReference[], + readonly references: vscode.ChatPromptReference[], readonly participant: string, ) { } } diff --git a/src/vscode-dts/vscode.proposed.chatParticipant.d.ts b/src/vscode-dts/vscode.proposed.chatParticipant.d.ts index 66e943c4330..9ace1a09551 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipant.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipant.d.ts @@ -32,9 +32,12 @@ declare module 'vscode' { /** * The references that were used in this message. */ - readonly references: ChatValueReference[]; + readonly references: ChatPromptReference[]; - private constructor(prompt: string, command: string | undefined, references: ChatValueReference[], participant: string); + /** + * @hidden + */ + private constructor(prompt: string, command: string | undefined, references: ChatPromptReference[], participant: string); } /** @@ -61,12 +64,18 @@ declare module 'vscode' { */ readonly command?: string; + /** + * @hidden + */ private constructor(response: ReadonlyArray, result: ChatResult, participant: string); } + /** + * Extra context passed to a participant. + */ export interface ChatContext { /** - * All of the chat messages so far in the current chat session. + * All of the chat messages so far in the current chat session. Currently, only chat messages for the current participant are included. */ readonly history: ReadonlyArray; } @@ -80,16 +89,6 @@ declare module 'vscode' { */ message: string; - /** - * If partial markdown content was sent over the {@link ChatRequestHandler handler}'s response stream before the response terminated, then this flag - * can be set to true and it will be rendered with incomplete markdown features patched up. - * - * For example, if the response terminated after sending part of a triple-backtick code block, then the editor will - * render it as a complete code block. - */ - // TODO@API: consider to have this always on, the presence of an error is a good indicator - responseIsIncomplete?: boolean; - /** * If set to true, the response will be partly blurred out. */ @@ -229,26 +228,19 @@ declare module 'vscode' { onDidReceiveFeedback: Event; /** - * Dispose this participant and free resources + * Dispose this participant and free resources. */ dispose(): void; } - export interface ChatValueReference { + export interface ChatPromptReference { /** - * A unique identifier for this reference. + * A unique identifier for this kind of reference. */ readonly id: string; /** - * The name of the reference. - * TODO@API should name be provided at all, or only ID? - */ - // TODO@API nuke it, add when needed - readonly name: string; - - /** - * The start and end index of the reference in the {@link ChatRequest.prompt prompt}. When undefined, the + * The start and end index of the reference in the {@link ChatRequest.prompt prompt}. When undefined, the reference was not part of the prompt text. * * *Note* that the indices take the leading `#`-character into account which means they can * used to modify the prompt as-is. @@ -282,18 +274,16 @@ declare module 'vscode' { */ readonly command: string | undefined; - /** * The list of references and their values that are referenced in the prompt. * - * *Note* that the prompt contains varibale references as authored and that it is up to the participant + * *Note* that the prompt contains references as authored and that it is up to the participant * to further modify the prompt, for instance by inlining reference values or creating links to * headings which contain the resolved values. References are sorted in reverse by their range * in the prompt. That means the last reference in the prompt is the first in this list. This simplifies * string-manipulation of the prompt. */ - // TODO@API: name ChatRequestReference, ChatPromptReference - readonly references: readonly ChatValueReference[]; + readonly references: readonly ChatPromptReference[]; } /** @@ -301,7 +291,6 @@ declare module 'vscode' { * which will be rendered in an appropriate way in the chat view. A participant can use the helper method for the type of content it wants to return, or it * can instantiate a {@link ChatResponsePart} and use the generic {@link ChatResponseStream.push} method to return it. */ - // TODO@API make them return void export interface ChatResponseStream { /** * Push a markdown part to this stream. Short-hand for @@ -309,48 +298,43 @@ declare module 'vscode' { * * @see {@link ChatResponseStream.push} * @param value A markdown string or a string that should be interpreted as markdown. The boolean form of {@link MarkdownString.isTrusted} is NOT supported. - * @returns This stream. */ - markdown(value: string | MarkdownString): ChatResponseStream; + markdown(value: string | MarkdownString): void; /** * Push an anchor part to this stream. Short-hand for * `push(new ChatResponseAnchorPart(value, title))`. * An anchor is an inline reference to some type of resource. * - * @param value A uri or location - * @param title An optional title that is rendered with value - * @returns This stream. + * @param value A uri, location, or symbol information. + * @param title An optional title that is rendered with value. */ - anchor(value: Uri | Location, title?: string): ChatResponseStream; + anchor(value: Uri | Location, title?: string): void; /** * Push a command button part to this stream. Short-hand for * `push(new ChatResponseCommandButtonPart(value, title))`. * * @param command A Command that will be executed when the button is clicked. - * @returns This stream. */ - button(command: Command): ChatResponseStream; + button(command: Command): void; /** * Push a filetree part to this stream. Short-hand for * `push(new ChatResponseFileTreePart(value))`. * * @param value File tree data. - * @param baseUri The base uri to which this file tree is relative to. - * @returns This stream. + * @param baseUri The base uri to which this file tree is relative. */ - filetree(value: ChatResponseFileTree[], baseUri: Uri): ChatResponseStream; + filetree(value: ChatResponseFileTree[], baseUri: Uri): void; /** * Push a progress part to this stream. Short-hand for * `push(new ChatResponseProgressPart(value))`. * * @param value A progress message - * @returns This stream. */ - progress(value: string): ChatResponseStream; + progress(value: string): void; /** * Push a reference to this stream. Short-hand for @@ -360,57 +344,144 @@ declare module 'vscode' { * * @param value A uri or location * @param iconPath Icon for the reference shown in UI - * @returns This stream. */ - reference(value: Uri | Location, iconPath?: Uri | ThemeIcon | { light: Uri; dark: Uri }): ChatResponseStream; + reference(value: Uri | Location, iconPath?: Uri | ThemeIcon | { light: Uri; dark: Uri }): void; /** * Pushes a part to this stream. * * @param part A response part, rendered or metadata */ - push(part: ChatResponsePart): ChatResponseStream; + push(part: ChatResponsePart): void; } + /** + * Represents a part of a chat response that is formatted as Markdown. + */ export class ChatResponseMarkdownPart { + /** + * A markdown string or a string that should be interpreted as markdown. + */ value: MarkdownString; /** - * @param value Note: The boolean form of {@link MarkdownString.isTrusted} is NOT supported. + * Create a new ChatResponseMarkdownPart. + * + * @param value A markdown string or a string that should be interpreted as markdown. The boolean form of {@link MarkdownString.isTrusted} is NOT supported. */ constructor(value: string | MarkdownString); } + /** + * Represents a file tree structure in a chat response. + */ export interface ChatResponseFileTree { + /** + * The name of the file or directory. + */ name: string; + + /** + * An array of child file trees, if the current file tree is a directory. + */ children?: ChatResponseFileTree[]; } + /** + * Represents a part of a chat response that is a file tree. + */ export class ChatResponseFileTreePart { + /** + * File tree data. + */ value: ChatResponseFileTree[]; + + /** + * The base uri to which this file tree is relative + */ baseUri: Uri; + + /** + * Create a new ChatResponseFileTreePart. + * @param value File tree data. + * @param baseUri The base uri to which this file tree is relative. + */ constructor(value: ChatResponseFileTree[], baseUri: Uri); } + /** + * Represents a part of a chat response that is an anchor, that is rendered as a link to a target. + */ export class ChatResponseAnchorPart { - value: Uri | Location | SymbolInformation; + /** + * The target of this anchor. + */ + value: Uri | Location; + + /** + * An optional title that is rendered with value. + */ title?: string; - constructor(value: Uri | Location | SymbolInformation, title?: string); + + /** + * Create a new ChatResponseAnchorPart. + * @param value A uri or location. + * @param title An optional title that is rendered with value. + */ + constructor(value: Uri | Location, title?: string); } + /** + * Represents a part of a chat response that is a progress message. + */ export class ChatResponseProgressPart { + /** + * The progress message + */ value: string; + + /** + * Create a new ChatResponseProgressPart. + * @param value A progress message + */ constructor(value: string); } + /** + * Represents a part of a chat response that is a reference, rendered separately from the content. + */ export class ChatResponseReferencePart { + /** + * The reference target. + */ value: Uri | Location; + + /** + * The icon for the reference. + */ iconPath?: Uri | ThemeIcon | { light: Uri; dark: Uri }; + + /** + * Create a new ChatResponseReferencePart. + * @param value A uri or location + * @param iconPath Icon for the reference shown in UI + */ constructor(value: Uri | Location, iconPath?: Uri | ThemeIcon | { light: Uri; dark: Uri }); } + /** + * Represents a part of a chat response that is a button that executes a command. + */ export class ChatResponseCommandButtonPart { + /** + * The command that will be executed when the button is clicked. + */ value: Command; + + /** + * Create a new ChatResponseCommandButtonPart. + * @param value A Command that will be executed when the button is clicked. + */ constructor(value: Command); } diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index c34b97baace..13d98419f84 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -132,12 +132,12 @@ declare module 'vscode' { * @param task If provided, a task to run while the progress is displayed. When the Thenable resolves, the progress will be marked complete in the UI, and the progress message will be updated to the resolved string if one is specified. * @returns This stream. */ - progress(value: string, task?: (progress: Progress) => Thenable): ChatResponseStream; + progress(value: string, task?: (progress: Progress) => Thenable): void; - textEdit(target: Uri, edits: TextEdit | TextEdit[]): ChatResponseStream; - markdownWithVulnerabilities(value: string | MarkdownString, vulnerabilities: ChatVulnerability[]): ChatResponseStream; - detectedParticipant(participant: string, command?: ChatCommand): ChatResponseStream; - push(part: ChatResponsePart | ChatResponseTextEditPart | ChatResponseDetectedParticipantPart | ChatResponseWarningPart | ChatResponseProgressPart2): ChatResponseStream; + textEdit(target: Uri, edits: TextEdit | TextEdit[]): void; + markdownWithVulnerabilities(value: string | MarkdownString, vulnerabilities: ChatVulnerability[]): void; + detectedParticipant(participant: string, command?: ChatCommand): void; + push(part: ChatResponsePart | ChatResponseTextEditPart | ChatResponseDetectedParticipantPart | ChatResponseWarningPart | ChatResponseProgressPart2): void; /** * Show an inline message in the chat view asking the user to confirm an action. @@ -149,7 +149,7 @@ declare module 'vscode' { * TODO@API should this be MarkdownString? * TODO@API should actually be a more generic function that takes an array of buttons */ - confirmation(title: string, message: string, data: any): ChatResponseStream; + confirmation(title: string, message: string, data: any): void; /** * Push a warning to this stream. Short-hand for @@ -158,11 +158,11 @@ declare module 'vscode' { * @param message A warning message * @returns This stream. */ - warning(message: string | MarkdownString): ChatResponseStream; + warning(message: string | MarkdownString): void; - reference(value: Uri | Location | { variableName: string; value?: Uri | Location }, iconPath?: Uri | ThemeIcon | { light: Uri; dark: Uri }): ChatResponseStream; + reference(value: Uri | Location | { variableName: string; value?: Uri | Location }, iconPath?: Uri | ThemeIcon | { light: Uri; dark: Uri }): void; - push(part: ExtendedChatResponsePart): ChatResponseStream; + push(part: ExtendedChatResponsePart): void; } /** @@ -300,6 +300,13 @@ declare module 'vscode' { readonly action: ChatCopyAction | ChatInsertAction | ChatTerminalAction | ChatCommandAction | ChatFollowupAction | ChatBugReportAction | ChatEditorAction; } + export interface ChatPromptReference { + /** + * TODO Needed for now to drive the variableName-type reference, but probably both of these should go away in the future. + */ + readonly name: string; + } + /** * The detail level of this chat variable value. */ From 1a9a2be60e93476b0833549870bf2d17acf3f6d2 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Tue, 14 May 2024 17:48:54 -0700 Subject: [PATCH 183/357] Fix broken chat input placeholder and decorations (#212759) --- src/vs/workbench/contrib/chat/browser/chatWidget.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 6fc4e35aed7..45578249ffc 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -584,6 +584,8 @@ export class ChatWidget extends Disposable implements IChatWidget { } this._onDidChangeContentHeight.fire(); })); + this._register(this.inputEditor.onDidChangeModelContent(() => this.parsedChatRequest = undefined)); + this._register(this.chatAgentService.onDidChangeAgents(() => this.parsedChatRequest = undefined)); this._register(this.inputPart.onDidChangeAttachedContext(() => { if (this.bodyDimension) { this.layout(this.bodyDimension.height, this.bodyDimension.width); From 28c4d6b6a189def65a21e707b80861d8a45fdffb Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Tue, 14 May 2024 17:59:39 -0700 Subject: [PATCH 184/357] Prevent overflowing text (#212761) --- src/vs/workbench/contrib/chat/browser/media/chat.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 6be7e3e6f59..391a2bcf4b9 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -216,7 +216,7 @@ .interactive-item-container .value { white-space: normal; - word-wrap: break-word; + overflow-wrap: anywhere; } .interactive-item-container .value > :last-child.rendered-markdown > :last-child { From 5f7906e91b2633350cfb45446e3818bf8979388d Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Tue, 14 May 2024 18:07:32 -0700 Subject: [PATCH 185/357] fix: allow multiple chat file references (#212756) * fix: allow multiple chat file references * refactor: register disposables, fix merge, cleanup * Reuse `onDidChangeHeight` --- .../chat/browser/actions/chatContextActions.ts | 2 +- .../contrib/chat/browser/chatInputPart.ts | 15 +++++++++++---- .../workbench/contrib/chat/browser/chatWidget.ts | 9 ++------- .../chat/browser/contrib/chatDynamicVariables.ts | 1 - .../contrib/chat/common/chatServiceImpl.ts | 2 +- .../contrib/chat/test/common/mockChatService.ts | 5 +---- 6 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index 8597a0dc35c..4827b26a7b9 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -73,7 +73,7 @@ class AttachContextAction extends Action2 { const context: { widget?: IChatWidget } | undefined = args[0]; const widget = context?.widget ?? widgetService.lastFocusedWidget; - widget?.attachContext(...picks.map((p) => ({ name: p.label, value: 'resource' in p && URI.isUri(p.resource) ? p.resource : undefined, id: p.id! }))); + widget?.attachContext(...picks.map((p) => ({ name: p.label, value: 'resource' in p && URI.isUri(p.resource) ? p.resource : undefined, id: 'resource' in p && URI.isUri(p.resource) ? `${SelectAndInsertFileAction.Name}:${p.resource.toString()}` : '' }))); } } } diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index e2fd33951eb..5f191e62655 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -87,15 +87,14 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private _onDidAcceptFollowup = this._register(new Emitter<{ followup: IChatFollowup; response: IChatResponseViewModel | undefined }>()); readonly onDidAcceptFollowup = this._onDidAcceptFollowup.event; - private _onDidChangeAttachedContext = this._register(new Emitter()); - readonly onDidChangeAttachedContext = this._onDidChangeAttachedContext.event; public get attachedContext() { return this._attachedContext; } private readonly _attachedContext = new Set(); - private readonly _contextResourceLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: new Emitter().event }); + private readonly _onDidChangeVisibility = this._register(new Emitter()); + private readonly _contextResourceLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility.event }); private inputEditorHeight = 0; private container!: HTMLElement; @@ -106,6 +105,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private readonly followupsDisposables = this._register(new DisposableStore()); private attachedContextContainer!: HTMLElement; + private readonly attachedContextDisposables = this._register(new DisposableStore()); private _inputPartHeight: number = 0; get inputPartHeight() { @@ -182,6 +182,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } } + setVisible(visible: boolean): void { + this._onDidChangeVisibility.fire(visible); + } + get element(): HTMLElement { return this.container; } @@ -425,6 +429,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private initAttachedContext(container: HTMLElement) { dom.clearNode(container); + this.attachedContextDisposables.clear(); dom.setVisibility(Boolean(this.attachedContext.size), this.attachedContextContainer); for (const attachment of this.attachedContext) { const widget = dom.append(container, $('.chat-attached-context-attachment.show-file-icons')); @@ -440,12 +445,14 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } const clearButton = new Button(widget, { supportIcons: true }); + this.attachedContextDisposables.add(clearButton); clearButton.icon = Codicon.close; const disp = clearButton.onDidClick(() => { this.attachedContext.delete(attachment); disp.dispose(); - this._onDidChangeAttachedContext.fire(); + this._onDidChangeHeight.fire(); }); + this.attachedContextDisposables.add(disp); } } diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 45578249ffc..30432395fe3 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -395,6 +395,7 @@ export class ChatWidget extends Disposable implements IChatWidget { this._visible = visible; this.visibleChangeCount++; this.renderer.setVisible(visible); + this.input.setVisible(visible); if (visible) { this._register(disposableTimeout(() => { @@ -586,12 +587,6 @@ export class ChatWidget extends Disposable implements IChatWidget { })); this._register(this.inputEditor.onDidChangeModelContent(() => this.parsedChatRequest = undefined)); this._register(this.chatAgentService.onDidChangeAgents(() => this.parsedChatRequest = undefined)); - this._register(this.inputPart.onDidChangeAttachedContext(() => { - if (this.bodyDimension) { - this.layout(this.bodyDimension.height, this.bodyDimension.width); - } - this._onDidChangeContentHeight.fire(); - })); } private onDidStyleChange(): void { @@ -712,9 +707,9 @@ export class ChatWidget extends Disposable implements IChatWidget { `${opts.prefix} ${editorValue}`; const isUserQuery = !opts || 'prefix' in opts; const result = await this.chatService.sendRequest(this.viewModel.sessionId, input, { location: this.location, parserContext: { selectedAgent: this._lastSelectedAgent }, attachedContext: [...this.inputPart.attachedContext.values()] }); - this.inputPart.attachedContext.clear(); if (result) { + this.inputPart.attachedContext.clear(); const inputState = this.collectInputState(); this.inputPart.acceptInput(isUserQuery ? input : undefined, isUserQuery ? inputState : undefined); this._onDidSubmitAgent.fire({ agent: result.agent, slashCommand: result.slashCommand }); diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts index 3b587a6d6bd..4c3fa0ee29c 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts @@ -131,7 +131,6 @@ export class SelectAndInsertFileAction extends Action2 { static readonly Item = { label: localize('allFiles', 'All Files'), description: localize('allFilesDescription', 'Search for relevant files in the workspace and provide context from them'), - id: 'vscode.file' }; static readonly ID = 'workbench.action.chat.selectAndInsertFile'; diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index 46f5708a031..8acb4f44e8d 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -429,7 +429,7 @@ export class ChatService extends Disposable implements IChatService { this.removeRequest(model.sessionId, request.id); - await this._sendRequestAsync(model, model.sessionId, request.message, attempt, enableCommandDetection, defaultAgent, location, options); + await this._sendRequestAsync(model, model.sessionId, request.message, attempt, enableCommandDetection, defaultAgent, location, options).responseCompletePromise; } async sendRequest(sessionId: string, request: string, options?: IChatSendRequestOptions): Promise { diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts index c432e2e2cb5..f2671c73f59 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts @@ -9,7 +9,7 @@ import { URI } from 'vs/base/common/uri'; import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatModel, IChatModel, IChatRequestModel, IChatRequestVariableData, ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { IChatCompleteResponse, IChatContentVariableReference, IChatDetail, IChatProviderInfo, IChatSendRequestData, IChatSendRequestOptions, IChatService, IChatTransferredSessionData, IChatUserActionEvent } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatCompleteResponse, IChatDetail, IChatProviderInfo, IChatSendRequestData, IChatSendRequestOptions, IChatService, IChatTransferredSessionData, IChatUserActionEvent } from 'vs/workbench/contrib/chat/common/chatService'; export class MockChatService implements IChatService { _serviceBrand: undefined; @@ -77,7 +77,4 @@ export class MockChatService implements IChatService { transferChatSession(transferredSessionData: IChatTransferredSessionData, toWorkspace: URI): void { throw new Error('Method not implemented.'); } - attachContext(...context: IChatContentVariableReference[]): void { - return; - } } From 5e68ffd760038bb156894a87f896910be510accd Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Tue, 14 May 2024 19:08:31 -0700 Subject: [PATCH 186/357] Fix #212717 (#212763) --- .../chat/browser/chatParticipantContributions.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts b/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts index 07e458fdca2..373dc080b2f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts @@ -5,7 +5,7 @@ import { isNonEmptyArray } from 'vs/base/common/arrays'; import { Codicon } from 'vs/base/common/codicons'; -import { DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { localize, localize2 } from 'vs/nls'; import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; @@ -263,7 +263,13 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { return viewContainer; } + private hasRegisteredDefaultParticipantView = false; private registerDefaultParticipantView(defaultParticipantDescriptor: IRawChatParticipantContribution): IDisposable { + if (this.hasRegisteredDefaultParticipantView) { + this.logService.warn(`Tried to register a second default chat participant view for "${defaultParticipantDescriptor.id}"`); + return Disposable.None; + } + // Register View const name = defaultParticipantDescriptor.fullName ?? defaultParticipantDescriptor.name; const viewDescriptor: IViewDescriptor[] = [{ @@ -276,9 +282,11 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { canMoveView: true, ctorDescriptor: new SyncDescriptor(ChatViewPane), }]; + this.hasRegisteredDefaultParticipantView = true; Registry.as(ViewExtensions.ViewsRegistry).registerViews(viewDescriptor, this._viewContainer); return toDisposable(() => { + this.hasRegisteredDefaultParticipantView = false; Registry.as(ViewExtensions.ViewsRegistry).deregisterViews(viewDescriptor, this._viewContainer); }); } From 7680db2a117f3f245aa3ef8b7568b6cc222973e6 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 15 May 2024 09:31:04 +0200 Subject: [PATCH 187/357] Move rerun request back to prime spot, return with intent detection back as link (#212775) * Bring back "Rerun Request" to its prominent spot https://github.com/microsoft/vscode-copilot/issues/5604 * move rerun without gesture back into title fixes https://github.com/microsoft/vscode-copilot/issues/5275 --- .../chat/browser/actions/chatTitleActions.ts | 92 +------------------ .../contrib/chat/browser/chatListRenderer.ts | 30 ++++-- .../contrib/chat/browser/chatWidget.ts | 6 ++ .../contrib/chat/browser/media/chat.css | 5 + .../browser/inlineChat.contribution.ts | 1 + .../inlineChat/browser/inlineChatActions.ts | 31 ++++++- 6 files changed, 68 insertions(+), 97 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts index c91cf01aefd..4a2455ea71f 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts @@ -8,15 +8,14 @@ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { marked } from 'vs/base/common/marked/marked'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; -import { localize, localize2 } from 'vs/nls'; -import { Action2, MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; +import { localize2 } from 'vs/nls'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits'; import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; -import { ChatTreeItem, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; -import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { CONTEXT_CHAT_LOCATION, CONTEXT_CHAT_RESPONSE_SUPPORT_ISSUE_REPORTING, CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION, CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_DETECTED_AGENT_COMMAND, CONTEXT_RESPONSE_FILTERED, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; +import { CONTEXT_CHAT_RESPONSE_SUPPORT_ISSUE_REPORTING, CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION, CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_FILTERED, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatService, ChatAgentVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { isRequestVM, isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; @@ -254,89 +253,6 @@ export function registerChatTitleActions() { } } }); - - const rerunMenu = MenuId.for('ChatMessageTitle#Rerun'); - - MenuRegistry.appendMenuItem(MenuId.ChatMessageTitle, { - submenu: rerunMenu, - title: localize('reunmenu', "Rerun..."), - icon: Codicon.refresh, - group: 'navigation', - order: -10, - when: ContextKeyExpr.and(CONTEXT_RESPONSE, CONTEXT_CHAT_LOCATION.isEqualTo(ChatAgentLocation.Editor)) // TODO@jrieken needs extension adoption - - }); - - registerAction2(class RerunAction extends Action2 { - constructor() { - super({ - id: 'workbench.action.chat.rerun', - title: localize2('chat.rerun.label', "Rerun Request"), - f1: false, - category: CHAT_CATEGORY, - icon: Codicon.refresh, - precondition: CONTEXT_CHAT_LOCATION.isEqualTo(ChatAgentLocation.Editor), // TODO@jrieken needs extension adoption - menu: { - id: rerunMenu, - group: 'navigation', - order: -1, - } - }); - } - - async run(accessor: ServicesAccessor, ...args: [ChatTreeItem | unknown]) { - const chatWidgetService = accessor.get(IChatWidgetService); - const chatService = accessor.get(IChatService); - const widget = chatWidgetService.lastFocusedWidget; - let item = args[0]; - if (!isResponseVM(item)) { - item = widget?.getFocus(); - } - if (!isResponseVM(item) || !widget) { - return; - } - const request = chatService.getSession(item.sessionId)?.getRequests().find(candidate => candidate.id === item.requestId); - if (request) { - await chatService.resendRequest(request, { noCommandDetection: false, attempt: request.attempt + 1, location: widget.location }); - } - } - }); - - registerAction2(class RerunWithoutCommandDetectionAction extends Action2 { - constructor() { - super({ - id: 'workbench.action.chat.rerunWithoutCommandDetection', - title: localize2('chat.rerunWithoutCommandDetection.label', "Rerun without Command Detection"), - f1: false, - category: CHAT_CATEGORY, - icon: Codicon.refresh, - precondition: CONTEXT_CHAT_LOCATION.isEqualTo(ChatAgentLocation.Editor), // TODO@jrieken needs extension adoption - menu: { - when: CONTEXT_RESPONSE_DETECTED_AGENT_COMMAND, - id: rerunMenu, - group: 'navigation', - order: -1, - } - }); - } - - async run(accessor: ServicesAccessor, ...args: any[]) { - const chatWidgetService = accessor.get(IChatWidgetService); - const chatService = accessor.get(IChatService); - const widget = chatWidgetService.lastFocusedWidget; - let item = args[0]; - if (!isResponseVM(item)) { - item = widget?.getFocus(); - } - if (!isResponseVM(item) || !widget) { - return; - } - const request = chatService.getSession(item.sessionId)?.getRequests().find(candidate => candidate.id === item.requestId); - if (request) { - await chatService.resendRequest(request, { noCommandDetection: true, attempt: request.attempt, location: widget.location }); - } - } - }); } interface MarkdownContent { diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 687d7361c98..0e3c34fec24 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -78,6 +78,7 @@ import { ITrustedDomainService } from 'vs/workbench/contrib/url/browser/trustedD import { IMarkdownVulnerability, annotateSpecialMarkdownContent } from '../common/annotations'; import { CodeBlockModelCollection } from '../common/codeBlockModelCollection'; import { IChatListItemRendererOptions } from './chat'; +import { renderFormattedText } from 'vs/base/browser/formattedTextRenderer'; const $ = dom.$; @@ -124,6 +125,9 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer()); readonly onDidClickFollowup: Event = this._onDidClickFollowup.event; + private readonly _onDidClickRerunWithAgentOrCommandDetection = new Emitter(); + readonly onDidClickRerunWithAgentOrCommandDetection: Event = this._onDidClickRerunWithAgentOrCommandDetection.event; + protected readonly _onDidChangeItemHeight = this._register(new Emitter()); readonly onDidChangeItemHeight: Event = this._onDidChangeItemHeight.event; @@ -396,19 +400,31 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { + this._onDidClickRerunWithAgentOrCommandDetection.fire(element); + }, + } + })); - templateData.detail.textContent = progressMsg; + } else if (!element.isComplete) { + templateData.detail.textContent = GeneratingPhrase; + } } private renderAvatar(element: ChatTreeItem, templateData: IChatListItemTemplate): void { diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 30432395fe3..7144f1b7948 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -436,6 +436,12 @@ export class ChatWidget extends Disposable implements IChatWidget { // is this used anymore? this.acceptInput(item.message); })); + this._register(this.renderer.onDidClickRerunWithAgentOrCommandDetection(item => { + const request = this.chatService.getSession(item.sessionId)?.getRequests().find(candidate => candidate.id === item.requestId); + if (request) { + this.chatService.resendRequest(request, { noCommandDetection: true, attempt: request.attempt, location: this.location }).catch(e => this.logService.error('FAILED to rerun request', e)); + } + })); this.tree = >scopedInstantiationService.createInstance( WorkbenchObjectTree, diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 391a2bcf4b9..8b09ad3c402 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -53,6 +53,11 @@ color: var(--vscode-descriptionForeground); } +.interactive-item-container .header .detail-container .detail .agentOrSlashCommandDetected A { + cursor: pointer; + color: var(--vscode-textLink-foreground); +} + .interactive-item-container .chat-animated-ellipsis { display: inline-block; width: 11px; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts index 5effa9d61b6..2044d45b2e3 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts @@ -40,6 +40,7 @@ registerAction2(InlineChatActions.DiscardHunkAction); registerAction2(InlineChatActions.DiscardAction); registerAction2(InlineChatActions.DiscardToClipboardAction); registerAction2(InlineChatActions.DiscardUndoToNewFileAction); +registerAction2(InlineChatActions.RerunAction); registerAction2(InlineChatActions.CancelSessionAction); registerAction2(InlineChatActions.MoveToNextHunk); registerAction2(InlineChatActions.MoveToPreviousHunk); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index 7ff7d0ab205..fe495060020 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -11,7 +11,7 @@ import { EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/em import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { InlineChatController, InlineChatRunOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; -import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_PROVIDER, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_VISIBLE, MENU_INLINE_CHAT_WIDGET_DISCARD, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_EDIT_MODE, EditMode, CTX_INLINE_CHAT_DOCUMENT_CHANGED, CTX_INLINE_CHAT_DID_EDIT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, ACTION_ACCEPT_CHANGES, CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChatResponseTypes, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, MENU_INLINE_CHAT_WIDGET, ACTION_TOGGLE_DIFF } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_PROVIDER, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_VISIBLE, MENU_INLINE_CHAT_WIDGET_DISCARD, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_EDIT_MODE, EditMode, CTX_INLINE_CHAT_DOCUMENT_CHANGED, CTX_INLINE_CHAT_DID_EDIT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, ACTION_ACCEPT_CHANGES, CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChatResponseTypes, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, MENU_INLINE_CHAT_WIDGET, ACTION_TOGGLE_DIFF, ACTION_REGENERATE_RESPONSE } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { localize, localize2 } from 'vs/nls'; import { Action2, IAction2Options, MenuRegistry } from 'vs/platform/actions/common/actions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; @@ -29,6 +29,7 @@ import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { ILogService } from 'vs/platform/log/common/log'; +import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; CommandsRegistry.registerCommandAlias('interactiveEditor.start', 'inlineChat.start'); CommandsRegistry.registerCommandAlias('interactive.acceptChanges', ACTION_ACCEPT_CHANGES); @@ -356,7 +357,8 @@ export class ToggleDiffForChange extends AbstractInlineChatAction { { id: MENU_INLINE_CHAT_WIDGET_STATUS, group: '1_main', - when: ContextKeyExpr.and(CTX_INLINE_CHAT_EDIT_MODE.isEqualTo(EditMode.Live), CTX_INLINE_CHAT_CHANGE_HAS_DIFF) + when: ContextKeyExpr.and(CTX_INLINE_CHAT_EDIT_MODE.isEqualTo(EditMode.Live), CTX_INLINE_CHAT_CHANGE_HAS_DIFF), + order: 10, } ] }); @@ -564,3 +566,28 @@ export class ViewInChatAction extends AbstractInlineChatAction { } } +export class RerunAction extends AbstractInlineChatAction { + constructor() { + super({ + id: ACTION_REGENERATE_RESPONSE, + title: localize2('chat.rerun.label', "Rerun Request"), + f1: false, + icon: Codicon.refresh, + menu: { + id: MENU_INLINE_CHAT_WIDGET_STATUS, + group: '0_main', + order: 5, + } + }); + } + + override async runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: any[]): Promise { + const chatService = accessor.get(IChatService); + const model = ctrl.chatWidget.viewModel?.model; + + const lastRequest = model?.getRequests().at(-1); + if (lastRequest) { + await chatService.resendRequest(lastRequest, { noCommandDetection: false, attempt: lastRequest.attempt + 1, location: ctrl.chatWidget.location }); + } + } +} From 6ade685193ea99f0fe1983ad82e7bf3870d1a768 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Wed, 15 May 2024 09:42:20 +0200 Subject: [PATCH 188/357] Fix hovers on empty group watermarks (#212777) fixes #212355 --- .../browser/parts/editor/editorGroupWatermark.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts b/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts index 77d8a445857..c9ac110bc23 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts @@ -62,7 +62,7 @@ export class EditorGroupWatermark extends Disposable { private readonly transientDisposables = this._register(new DisposableStore()); private enabled: boolean = false; private workbenchState: WorkbenchState; - private keybindingLabel?: KeybindingLabel; + private keybindingLabels = new Set(); constructor( container: HTMLElement, @@ -137,6 +137,9 @@ export class EditorGroupWatermark extends Disposable { const update = () => { clearNode(box); + this.keybindingLabels.forEach(label => label.dispose()); + this.keybindingLabels.clear(); + for (const entry of selected) { const keys = this.keybindingService.lookupKeybinding(entry.id); if (!keys) { @@ -146,9 +149,9 @@ export class EditorGroupWatermark extends Disposable { const dt = append(dl, $('dt')); dt.textContent = entry.text; const dd = append(dl, $('dd')); - this.keybindingLabel?.dispose(); - this.keybindingLabel = new KeybindingLabel(dd, OS, { renderUnboundKeybindings: true, ...defaultKeybindingLabelStyles }); - this.keybindingLabel.set(keys); + const label = new KeybindingLabel(dd, OS, { renderUnboundKeybindings: true, ...defaultKeybindingLabelStyles }); + label.set(keys); + this.keybindingLabels.add(label); } }; @@ -164,6 +167,6 @@ export class EditorGroupWatermark extends Disposable { override dispose(): void { super.dispose(); this.clear(); - this.keybindingLabel?.dispose(); + this.keybindingLabels.forEach(label => label.dispose()); } } From c1f1d6aba36bf53ff1074a86546407078f75c3ef Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 15 May 2024 09:51:12 +0200 Subject: [PATCH 189/357] update distro (#212776) --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index f7c6f37bbd0..5cc7a43e090 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.90.0", - "distro": "b545c05b2646e72d172e58e08b9315e5e8478296", + "distro": "26ff176dca24e02834457f98dd01bb4513dda29d", "author": { "name": "Microsoft Corporation" }, @@ -229,4 +229,4 @@ "optionalDependencies": { "windows-foreground-love": "0.5.0" } -} +} \ No newline at end of file From 8693728cc3a010b65ed07a0f74121e88276b6bf2 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 15 May 2024 10:23:10 +0200 Subject: [PATCH 190/357] voice - cleanup synthesize actions (#212680) --- .../browser/workbench.contribution.ts | 2 +- src/vs/workbench/contrib/chat/browser/chat.ts | 2 +- .../contrib/chat/browser/chatWidget.ts | 6 +- .../actions/voiceChatActions.ts | 138 ++++++++++++------ .../chat/browser/terminalChatController.ts | 2 +- .../chat/browser/terminalChatWidget.ts | 6 +- 6 files changed, 105 insertions(+), 51 deletions(-) diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index 41a4b4041d3..ce003932bf3 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -63,7 +63,7 @@ const registry = Registry.as(ConfigurationExtensions.Con }, 'workbench.editor.alwaysShowEditorActions': { 'type': 'boolean', - 'markdownDescription': localize('alwaysShowEditorActions', "Controls wheater to always show the editor actions, even when the editor group is not active."), + 'markdownDescription': localize('alwaysShowEditorActions', "Controls whether to always show the editor actions, even when the editor group is not active."), 'default': false }, 'workbench.editor.wrapTabs': { diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index 27edc8eca4b..3a606450f2d 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -128,7 +128,7 @@ export type IChatWidgetViewContext = IChatViewViewContext | IChatResourceViewCon export interface IChatWidget { readonly onDidChangeViewModel: Event; readonly onDidAcceptInput: Event; - readonly onDidHideInput: Event; + readonly onDidHide: Event; readonly onDidSubmitAgent: Event<{ agent: IChatAgentData; slashCommand?: IChatAgentCommand }>; readonly onDidChangeParsedInput: Event; readonly location: ChatAgentLocation; diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 7144f1b7948..cdd2f1f8bee 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -96,8 +96,8 @@ export class ChatWidget extends Disposable implements IChatWidget { private _onDidAcceptInput = this._register(new Emitter()); readonly onDidAcceptInput = this._onDidAcceptInput.event; - private _onDidHideInput = this._register(new Emitter()); - readonly onDidHideInput = this._onDidHideInput.event; + private _onDidHide = this._register(new Emitter()); + readonly onDidHide = this._onDidHide.event; private _onDidChangeParsedInput = this._register(new Emitter()); readonly onDidChangeParsedInput = this._onDidChangeParsedInput.event; @@ -406,7 +406,7 @@ export class ChatWidget extends Disposable implements IChatWidget { } }, 0)); } else if (wasVisible) { - this._onDidHideInput.fire(); + this._onDidHide.fire(); } } diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts index 8b1423ae64c..e0a21d0ccb8 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts @@ -45,7 +45,7 @@ import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/com import { InlineChatController } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; -import { HasSpeechProvider, ISpeechService, ITextToSpeechSession, KeywordRecognitionStatus, SpeechToTextInProgress, SpeechToTextStatus, TextToSpeechInProgress } from 'vs/workbench/contrib/speech/common/speechService'; +import { HasSpeechProvider, ISpeechService, ITextToSpeechSession, KeywordRecognitionStatus, SpeechToTextInProgress, SpeechToTextStatus, TextToSpeechStatus, TextToSpeechInProgress as GlobalTextToSpeechInProgress } from 'vs/workbench/contrib/speech/common/speechService'; import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalChatContextKeys, TerminalChatController } from 'vs/workbench/contrib/terminal/browser/terminalContribExports'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -69,9 +69,9 @@ const FocusInChatInput = ContextKeyExpr.or(CTX_INLINE_CHAT_FOCUSED, CONTEXT_IN_C const AnyChatRequestInProgress = ContextKeyExpr.or(CONTEXT_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST, TerminalChatContextKeys.requestActive); // Scoped Context Keys (set on per-chat-context scoped context key service) -const SCOPED_VOICE_CHAT_GETTING_READY = new RawContextKey('scopedVoiceChatGettingReady', false, { type: 'boolean', description: localize('scopedVoiceChatGettingReady', "True when getting ready for receiving voice input from the microphone for voice chat. This key is only defined scoped, per chat context.") }); -const SCOPED_VOICE_CHAT_IN_PROGRESS = new RawContextKey('scopedVoiceChatInProgress', undefined, { type: 'string', description: localize('scopedVoiceChatInProgress', "Defined as a location where voice recording from microphone is in progress for voice chat. This key is only defined scoped, per chat context.") }); -const ScopedVoiceChatInProgress = ContextKeyExpr.or(...VoiceChatSessionContexts.map(context => SCOPED_VOICE_CHAT_IN_PROGRESS.isEqualTo(context))); +const ScopedVoiceChatGettingReady = new RawContextKey('scopedVoiceChatGettingReady', false, { type: 'boolean', description: localize('scopedVoiceChatGettingReady', "True when getting ready for receiving voice input from the microphone for voice chat. This key is only defined scoped, per chat context.") }); +const ScopedVoiceChatInProgress = new RawContextKey('scopedVoiceChatInProgress', undefined, { type: 'string', description: localize('scopedVoiceChatInProgress', "Defined as a location where voice recording from microphone is in progress for voice chat. This key is only defined scoped, per chat context.") }); +const AnyScopedVoiceChatInProgress = ContextKeyExpr.or(...VoiceChatSessionContexts.map(context => ScopedVoiceChatInProgress.isEqualTo(context))); enum VoiceChatSessionState { Stopped = 1, @@ -178,9 +178,9 @@ class VoiceChatSessionControllerFactory { return undefined; } - private static createContextKeyController(contextKeyService: IContextKeyService, context: VoiceChatSessionContext): (state: VoiceChatSessionState) => void { - const contextVoiceChatGettingReady = SCOPED_VOICE_CHAT_GETTING_READY.bindTo(contextKeyService); - const contextVoiceChatInProgress = SCOPED_VOICE_CHAT_IN_PROGRESS.bindTo(contextKeyService); + private static createChatContextKeyController(contextKeyService: IContextKeyService, context: VoiceChatSessionContext): (state: VoiceChatSessionState) => void { + const contextVoiceChatGettingReady = ScopedVoiceChatGettingReady.bindTo(contextKeyService); + const contextVoiceChatInProgress = ScopedVoiceChatInProgress.bindTo(contextKeyService); return (state: VoiceChatSessionState) => { switch (state) { @@ -204,14 +204,14 @@ class VoiceChatSessionControllerFactory { return { context, onDidAcceptInput: chatWidget.onDidAcceptInput, - onDidHideInput: chatWidget.onDidHideInput, + onDidHideInput: chatWidget.onDidHide, focusInput: () => chatWidget.focusInput(), acceptInput: () => chatWidget.acceptInput(), updateInput: text => chatWidget.setInput(text), getInput: () => chatWidget.getInput(), setInputPlaceholder: text => chatWidget.setInputPlaceholder(text), clearInputPlaceholder: () => chatWidget.resetInputPlaceholder(), - updateState: VoiceChatSessionControllerFactory.createContextKeyController(chatWidget.scopedContextKeyService, context) + updateState: VoiceChatSessionControllerFactory.createChatContextKeyController(chatWidget.scopedContextKeyService, context) }; } @@ -220,14 +220,14 @@ class VoiceChatSessionControllerFactory { return { context, onDidAcceptInput: terminalChat.onDidAcceptInput, - onDidHideInput: terminalChat.onDidHideInput, + onDidHideInput: terminalChat.onDidHide, focusInput: () => terminalChat.focus(), acceptInput: () => terminalChat.acceptInput(), updateInput: text => terminalChat.updateInput(text, false), getInput: () => terminalChat.getInput(), setInputPlaceholder: text => terminalChat.setPlaceholder(text), clearInputPlaceholder: () => terminalChat.resetPlaceholder(), - updateState: VoiceChatSessionControllerFactory.createContextKeyController(terminalChat.scopedContextKeyService, context) + updateState: VoiceChatSessionControllerFactory.createChatContextKeyController(terminalChat.scopedContextKeyService, context) }; } } @@ -385,10 +385,11 @@ class VoiceChatSessions { } if ( - !this.accessibilityService.isScreenReaderOptimized() && // do not synthesize when screen reader is active + !this.accessibilityService.isScreenReaderOptimized() && // do not auto synthesize when screen reader is active this.configurationService.getValue(AccessibilityVoiceSettingId.AutoSynthesize) === true ) { - ChatSynthesizerSessions.getInstance(this.instantiationService).start(response); + const controller = this.instantiationService.invokeFunction(accessor => ChatSynthesizerSessionController.create(accessor, response)); + ChatSynthesizerSessions.getInstance(this.instantiationService).start(controller); } } } @@ -439,7 +440,7 @@ export class VoiceChatInChatViewAction extends VoiceChatWithHoldModeAction { constructor() { super({ id: VoiceChatInChatViewAction.ID, - title: localize2('workbench.action.chat.voiceChatInView.label', "Voice Chat in View"), + title: localize2('workbench.action.chat.voiceChatInView.label', "Voice Chat in Chat View"), category: CHAT_CATEGORY, precondition: ContextKeyExpr.and( CanVoiceChat, @@ -457,7 +458,7 @@ export class HoldToVoiceChatInChatViewAction extends Action2 { constructor() { super({ id: HoldToVoiceChatInChatViewAction.ID, - title: localize2('workbench.action.chat.holdToVoiceChatInChatView.label', "Hold to Voice Chat in View"), + title: localize2('workbench.action.chat.holdToVoiceChatInChatView.label', "Hold to Voice Chat in Chat View"), keybinding: { weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and( @@ -554,25 +555,25 @@ export class StartVoiceChatAction extends Action2 { keybinding: { weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and( - FocusInChatInput, // scope this action to chat input fields only - EditorContextKeys.focus.negate(), // do not steal the editor inline-chat keybinding - NOTEBOOK_EDITOR_FOCUSED.negate() // do not steal the notebook inline-chat keybinding + FocusInChatInput, // scope this action to chat input fields only + EditorContextKeys.focus.negate(), // do not steal the editor inline-chat keybinding + NOTEBOOK_EDITOR_FOCUSED.negate() // do not steal the notebook inline-chat keybinding ), primary: KeyMod.CtrlCmd | KeyCode.KeyI }, icon: Codicon.mic, precondition: ContextKeyExpr.and( CanVoiceChat, - SCOPED_VOICE_CHAT_GETTING_READY.negate(), // disable when voice chat is getting ready - AnyChatRequestInProgress?.negate(), // disable when any chat request is in progress - SpeechToTextInProgress.negate() // disable when speech to text is in progress + ScopedVoiceChatGettingReady.negate(), // disable when voice chat is getting ready + AnyChatRequestInProgress?.negate(), // disable when any chat request is in progress + SpeechToTextInProgress.negate() // disable when speech to text is in progress ), menu: [{ id: MenuId.ChatExecute, when: ContextKeyExpr.and( HasSpeechProvider, - TextToSpeechInProgress.negate(), // hide when text to speech is in progress - ScopedVoiceChatInProgress?.negate(), // hide when voice chat is in progress + ScopedChatSynthesisInProgress.negate(), // hide when text to speech is in progress + AnyScopedVoiceChatInProgress?.negate(), // hide when voice chat is in progress ), group: 'navigation', order: -1 @@ -580,8 +581,8 @@ export class StartVoiceChatAction extends Action2 { id: TerminalChatExecute, when: ContextKeyExpr.and( HasSpeechProvider, - TextToSpeechInProgress.negate(), // hide when text to speech is in progress - ScopedVoiceChatInProgress?.negate(), // hide when voice chat is in progress + ScopedChatSynthesisInProgress.negate(), // hide when text to speech is in progress + AnyScopedVoiceChatInProgress?.negate(), // hide when voice chat is in progress ), group: 'navigation', order: -1 @@ -621,12 +622,12 @@ export class StopListeningAction extends Action2 { precondition: GlobalVoiceChatInProgress, // need global context here because of `f1: true` menu: [{ id: MenuId.ChatExecute, - when: ScopedVoiceChatInProgress, + when: AnyScopedVoiceChatInProgress, group: 'navigation', order: -1 }, { id: TerminalChatExecute, - when: ScopedVoiceChatInProgress, + when: AnyScopedVoiceChatInProgress, group: 'navigation', order: -1 }] @@ -666,6 +667,37 @@ export class StopListeningAndSubmitAction extends Action2 { //#region Text to Speech +const ScopedChatSynthesisInProgress = new RawContextKey('scopedChatSynthesisInProgress', false, { type: 'boolean', description: localize('scopedChatSynthesisInProgress', "Defined as a location where voice recording from microphone is in progress for voice chat. This key is only defined scoped, per chat context.") }); + +interface IChatSynthesizerSessionController { + + readonly onDidHideChat: Event; + + readonly contextKeyService: IContextKeyService; + readonly response: IChatResponseModel; +} + +class ChatSynthesizerSessionController { + + static create(accessor: ServicesAccessor, response: IChatResponseModel): IChatSynthesizerSessionController { + const chatWidgetService = accessor.get(IChatWidgetService); + const contextKeyService = accessor.get(IContextKeyService); + + let chatWidget = chatWidgetService.getWidgetBySessionId(response.session.sessionId); + if (chatWidget?.location === ChatAgentLocation.Editor) { + // somehow for inline chat, the response session returns the wrong widget + // so we need to find the correct one by going through the last focused + chatWidget = chatWidgetService.lastFocusedWidget; + } + + return { + onDidHideChat: chatWidget?.onDidHide ?? Event.None, + contextKeyService: chatWidget?.scopedContextKeyService ?? contextKeyService, + response + }; + } +} + class ChatSynthesizerSessions { private static instance: ChatSynthesizerSessions | undefined = undefined; @@ -684,7 +716,7 @@ class ChatSynthesizerSessions { @IInstantiationService private readonly instantiationService: IInstantiationService ) { } - async start(response: IChatResponseModel): Promise { + async start(controller: IChatSynthesizerSessionController): Promise { // Stop running text-to-speech or speech-to-text sessions in chats this.stop(); @@ -692,16 +724,35 @@ class ChatSynthesizerSessions { const activeSession = this.activeSession = new CancellationTokenSource(); + const disposables = new DisposableStore(); + activeSession.token.onCancellationRequested(() => disposables.dispose()); + const session = await this.speechService.createTextToSpeechSession(activeSession.token, 'chat'); if (activeSession.token.isCancellationRequested) { return; } - if (response.isComplete) { - return this.synthesizeCompletedResponse(session, response); + disposables.add(controller.onDidHideChat(() => this.stop())); + + const scopedChatToSpeechInProgress = ScopedChatSynthesisInProgress.bindTo(controller.contextKeyService); + disposables.add(toDisposable(() => scopedChatToSpeechInProgress.set(false))); + + disposables.add(session.onDidChange(e => { + switch (e.status) { + case TextToSpeechStatus.Started: + scopedChatToSpeechInProgress.set(true); + break; + case TextToSpeechStatus.Stopped: + scopedChatToSpeechInProgress.set(false); + break; + } + })); + + if (controller.response.isComplete) { + return this.synthesizeCompletedResponse(session, controller.response); } else { - return this.synthesizePendingResponse(session, response, activeSession.token); + return this.synthesizePendingResponse(session, controller.response, activeSession.token); } } @@ -775,9 +826,9 @@ export class ReadChatResponseAloud extends Action2 { id: MenuId.ChatMessageTitle, when: ContextKeyExpr.and( CanVoiceChat, - CONTEXT_RESPONSE, // only for responses - TextToSpeechInProgress.negate(), // but not when already in progress - CONTEXT_RESPONSE_FILTERED.negate() // and not when response is filtered + CONTEXT_RESPONSE, // only for responses + ScopedChatSynthesisInProgress.negate(), // but not when already in progress + CONTEXT_RESPONSE_FILTERED.negate() // and not when response is filtered ), group: 'navigation' } @@ -785,12 +836,15 @@ export class ReadChatResponseAloud extends Action2 { } run(accessor: ServicesAccessor, ...args: any[]) { + const instantiationService = accessor.get(IInstantiationService); + const response = args[0]; if (!isResponseVM(response)) { return; } - ChatSynthesizerSessions.getInstance(accessor.get(IInstantiationService)).start(response.model); + const controller = ChatSynthesizerSessionController.create(accessor, response.model); + ChatSynthesizerSessions.getInstance(instantiationService).start(controller); } } @@ -805,7 +859,7 @@ export class StopReadAloud extends Action2 { title: localize2('workbench.action.speech.stopReadAloud', "Stop Reading Aloud"), f1: true, category: CHAT_CATEGORY, - precondition: TextToSpeechInProgress, + precondition: GlobalTextToSpeechInProgress, // need global context here because of `f1: true` keybinding: { weight: KeybindingWeight.WorkbenchContrib + 100, primary: KeyCode.Escape, @@ -813,13 +867,13 @@ export class StopReadAloud extends Action2 { menu: [ { id: MenuId.ChatExecute, - when: TextToSpeechInProgress, + when: ScopedChatSynthesisInProgress, group: 'navigation', order: -1 }, { id: TerminalChatExecute, - when: TextToSpeechInProgress, + when: ScopedChatSynthesisInProgress, group: 'navigation', order: -1 } @@ -834,14 +888,14 @@ export class StopReadAloud extends Action2 { export class StopReadChatItemAloud extends Action2 { - static readonly ID = 'workbench.action.chat.stopRadChatItemAloud'; + static readonly ID = 'workbench.action.chat.stopReadChatItemAloud'; constructor() { super({ id: StopReadChatItemAloud.ID, icon: Codicon.mute, - title: localize2('workbench.action.chat.stopRadChatItemAloud', "Stop Reading Aloud"), - precondition: TextToSpeechInProgress, + title: localize2('workbench.action.chat.stopReadChatItemAloud', "Stop Reading Aloud"), + precondition: ScopedChatSynthesisInProgress, keybinding: { weight: KeybindingWeight.WorkbenchContrib + 100, primary: KeyCode.Escape, @@ -850,7 +904,7 @@ export class StopReadChatItemAloud extends Action2 { { id: MenuId.ChatMessageTitle, when: ContextKeyExpr.and( - TextToSpeechInProgress, // only when in progress + ScopedChatSynthesisInProgress, // only when in progress CONTEXT_RESPONSE, // only for responses CONTEXT_RESPONSE_FILTERED.negate() // but not when response is filtered ), diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index de7e10f9916..077d3fec75c 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -78,7 +78,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr } readonly onDidAcceptInput = Event.filter(this._messages.event, m => m === Message.ACCEPT_INPUT, this._store); - get onDidHideInput() { return this.chatWidget?.onDidHideInput ?? Event.None; } + get onDidHide() { return this.chatWidget?.onDidHide ?? Event.None; } private _terminalAgentName = 'terminal'; private _terminalAgentId: string | undefined; diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 88583834d80..0b4ec9dd073 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -27,8 +27,8 @@ export class TerminalChatWidget extends Disposable { private readonly _container: HTMLElement; - private readonly _onDidHideInput = this._register(new Emitter()); - readonly onDidHideInput = this._onDidHideInput.event; + private readonly _onDidHide = this._register(new Emitter()); + readonly onDidHide = this._onDidHide.event; private readonly _inlineChatWidget: InlineChatWidget; public get inlineChatWidget(): InlineChatWidget { return this._inlineChatWidget; } @@ -179,7 +179,7 @@ export class TerminalChatWidget extends Disposable { this._inlineChatWidget.value = ''; this._instance.focus(); this._setTerminalOffset(undefined); - this._onDidHideInput.fire(); + this._onDidHide.fire(); } private _setTerminalOffset(offset: number | undefined) { if (offset === undefined || this._container.classList.contains('hide')) { From ffb2581f0866e2ec33d2853e593955e3e8535796 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 15 May 2024 10:25:25 +0200 Subject: [PATCH 191/357] remove `maxOutputTokens` for now (#212781) https://github.com/microsoft/vscode/issues/206265 --- src/vs/workbench/api/common/extHostLanguageModels.ts | 1 - src/vscode-dts/vscode.proposed.languageModels.d.ts | 6 ------ 2 files changed, 7 deletions(-) diff --git a/src/vs/workbench/api/common/extHostLanguageModels.ts b/src/vs/workbench/api/common/extHostLanguageModels.ts index c7b9c7f597b..a97ec1648ba 100644 --- a/src/vs/workbench/api/common/extHostLanguageModels.ts +++ b/src/vs/workbench/api/common/extHostLanguageModels.ts @@ -263,7 +263,6 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { version: data.metadata.version, name: data.metadata.name, maxInputTokens: data.metadata.maxInputTokens, - maxOutputTokens: data.metadata.maxOutputTokens, countTokens(text, token) { if (!that._allLanguageModelData.has(identifier)) { throw extHostTypes.LanguageModelError.NotFound(identifier); diff --git a/src/vscode-dts/vscode.proposed.languageModels.d.ts b/src/vscode-dts/vscode.proposed.languageModels.d.ts index 68d2b2f4705..f2614f8709e 100644 --- a/src/vscode-dts/vscode.proposed.languageModels.d.ts +++ b/src/vscode-dts/vscode.proposed.languageModels.d.ts @@ -144,12 +144,6 @@ declare module 'vscode' { */ readonly maxInputTokens: number; - /** - * The maximum number of tokens that a model can generate in a single response. - */ - // TODO@API leave it out for now - readonly maxOutputTokens: number; - /** * Make a chat request using a language model. * From 739d4803af9b3f4fe4a7cf2e0a96a8eb73c901e1 Mon Sep 17 00:00:00 2001 From: Sean McManus Date: Wed, 15 May 2024 02:10:15 -0700 Subject: [PATCH 192/357] Add /** */ to cpp/language-configurations.json (#211202) This fixes the issue at https://github.com/microsoft/vscode-cpptools/issues/12249 . This was removed in https://github.com/microsoft/vscode/commit/98fa77a679fbd1095d05c0fcb381d6e9b775d743 . Then PR https://github.com/microsoft/vscode/pull/160357 added /* */ autoClosingPair. --- extensions/cpp/language-configuration.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extensions/cpp/language-configuration.json b/extensions/cpp/language-configuration.json index 3a5459401f9..0bf8df9dc01 100644 --- a/extensions/cpp/language-configuration.json +++ b/extensions/cpp/language-configuration.json @@ -14,7 +14,8 @@ { "open": "(", "close": ")" }, { "open": "'", "close": "'", "notIn": ["string", "comment"] }, { "open": "\"", "close": "\"", "notIn": ["string"] }, - { "open": "/*", "close": "*/", "notIn": ["string", "comment"] } + { "open": "/*", "close": "*/", "notIn": ["string", "comment"] }, + { "open": "/**", "close": " */", "notIn": ["string"] } ], "surroundingPairs": [ ["{", "}"], From 0891d3031c3592800237c8055ee1f79ed859506c Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 15 May 2024 11:25:43 +0200 Subject: [PATCH 193/357] fix type checks #211878 (#212790) --- .../browser/extensionFeaturesTab.ts | 3 +-- .../browser/extensions.contribution.ts | 10 ++++---- .../extensions/browser/extensionsActions.ts | 10 ++++---- .../extensions/browser/extensionsViewer.ts | 4 +-- .../extensions/browser/extensionsViews.ts | 3 +-- .../browser/keymapRecommendations.ts | 2 +- .../browser/userDataProfile.ts | 6 ++--- .../browser/userDataSyncConflictsView.ts | 2 +- .../userDataSync/browser/userDataSyncViews.ts | 25 +++++++++++-------- 9 files changed, 34 insertions(+), 31 deletions(-) diff --git a/src/vs/workbench/contrib/extensions/browser/extensionFeaturesTab.ts b/src/vs/workbench/contrib/extensions/browser/extensionFeaturesTab.ts index b22715ac423..42680a4ba1f 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionFeaturesTab.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionFeaturesTab.ts @@ -13,7 +13,6 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { localize } from 'vs/nls'; import { WorkbenchList } from 'vs/platform/list/browser/listService'; -import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { getExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { Button } from 'vs/base/browser/ui/button/button'; @@ -240,7 +239,7 @@ export class ExtensionFeaturesTab extends Themable { multipleSelectionSupport: false, setRowLineHeight: false, horizontalScrolling: false, - accessibilityProvider: >{ + accessibilityProvider: { getAriaLabel(extensionFeature: IExtensionFeatureDescriptor | null): string { return extensionFeature?.label ?? ''; }, diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 71b704376e4..57622bd5f09 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -6,7 +6,7 @@ import { localize, localize2 } from 'vs/nls'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { Registry } from 'vs/platform/registry/common/platform'; -import { MenuRegistry, MenuId, registerAction2, Action2, ISubmenuItem, IMenuItem, IAction2Options } from 'vs/platform/actions/common/actions'; +import { MenuRegistry, MenuId, registerAction2, Action2, IMenuItem, IAction2Options } from 'vs/platform/actions/common/actions'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ExtensionsLocalizedLabel, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel, InstallOperation, EXTENSION_INSTALL_SOURCE_CONTEXT, ExtensionInstallSource } from 'vs/platform/extensionManagement/common/extensionManagement'; import { EnablementState, IExtensionManagementServerService, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService, extensionsConfigurationNodeBase } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; @@ -635,7 +635,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi }); const autoUpdateExtensionsSubMenu = new MenuId('autoUpdateExtensionsSubMenu'); - MenuRegistry.appendMenuItem(MenuId.ViewContainerTitle, { + MenuRegistry.appendMenuItem(MenuId.ViewContainerTitle, { submenu: autoUpdateExtensionsSubMenu, title: localize('configure auto updating extensions', "Auto Update Extensions"), when: ContextKeyExpr.and(ContextKeyExpr.equals('viewContainer', VIEWLET_ID), CONTEXT_HAS_GALLERY), @@ -930,7 +930,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi }); const extensionsFilterSubMenu = new MenuId('extensionsFilterSubMenu'); - MenuRegistry.appendMenuItem(extensionsSearchActionsMenu, { + MenuRegistry.appendMenuItem(extensionsSearchActionsMenu, { submenu: extensionsFilterSubMenu, title: localize('filterExtensions', "Filter Extensions..."), group: 'navigation', @@ -1016,7 +1016,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi }); const extensionsCategoryFilterSubMenu = new MenuId('extensionsCategoryFilterSubMenu'); - MenuRegistry.appendMenuItem(extensionsFilterSubMenu, { + MenuRegistry.appendMenuItem(extensionsFilterSubMenu, { submenu: extensionsCategoryFilterSubMenu, title: localize('filter by category', "Category"), when: CONTEXT_HAS_GALLERY, @@ -1129,7 +1129,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi }); const extensionsSortSubMenu = new MenuId('extensionsSortSubMenu'); - MenuRegistry.appendMenuItem(extensionsFilterSubMenu, { + MenuRegistry.appendMenuItem(extensionsFilterSubMenu, { submenu: extensionsSortSubMenu, title: localize('sorty by', "Sort By"), when: ContextKeyExpr.and(ContextKeyExpr.or(CONTEXT_HAS_GALLERY, DefaultViewsContext)), diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 1fd2e6152b3..a2f80109939 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -39,7 +39,7 @@ import { PICK_WORKSPACE_FOLDER_COMMAND_ID } from 'vs/workbench/browser/actions/w import { INotificationService, IPromptChoice, Severity } from 'vs/platform/notification/common/notification'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IQuickPickItem, IQuickInputService, IQuickPickSeparator, QuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { IQuickPickItem, IQuickInputService, QuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { CancellationToken } from 'vs/base/common/cancellation'; import { alert } from 'vs/base/browser/ui/aria/aria'; import { IWorkbenchThemeService, IWorkbenchTheme, IWorkbenchColorTheme, IWorkbenchFileIconTheme, IWorkbenchProductIconTheme } from 'vs/workbench/services/themes/common/workbenchThemeService'; @@ -1666,8 +1666,8 @@ function getQuickPickEntries(themes: IWorkbenchTheme[], currentTheme: IWorkbench } } if (showCurrentTheme) { - picks.push({ type: 'separator', label: localize('current', "current") }); - picks.push({ label: currentTheme.label, id: currentTheme.id }); + picks.push({ type: 'separator', label: localize('current', "current") }); + picks.push({ label: currentTheme.label, id: currentTheme.id }); } return picks; } @@ -2086,7 +2086,7 @@ export abstract class AbstractConfigureRecommendedExtensionsAction extends Actio .then(reference => { const position = reference.object.textEditorModel.getPositionAt(offset); reference.dispose(); - return { + return { startLineNumber: position.lineNumber, startColumn: position.column, endLineNumber: position.lineNumber, @@ -2667,7 +2667,7 @@ export class ReinstallAction extends Action { label: extension.displayName, description: extension.identifier.id, extension, - } as (IQuickPickItem & { extension: IExtension }); + }; }); return entries; }); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts index 33683a48388..2fa59067e8b 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts @@ -23,7 +23,7 @@ import { listFocusForeground, listFocusBackground, foreground, editorBackground import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; -import { IListAccessibilityProvider, IListStyles } from 'vs/base/browser/ui/list/listWidget'; +import { IListStyles } from 'vs/base/browser/ui/list/listWidget'; import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; import { IStyleOverride } from 'vs/platform/theme/browser/defaultStyles'; import { getAriaLabelForExtension } from 'vs/workbench/contrib/extensions/browser/extensionsViews'; @@ -264,7 +264,7 @@ export class ExtensionsTree extends WorkbenchAsyncDataTree>{ + accessibilityProvider: { getAriaLabel(extensionData: IExtensionData): string { return getAriaLabelForExtension(extensionData.extension); }, diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index 63d1feb8d16..4dcb55bb040 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -45,7 +45,6 @@ import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; -import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; import { isVirtualWorkspace } from 'vs/platform/workspace/common/virtualWorkspace'; @@ -207,7 +206,7 @@ export class ExtensionsListView extends ViewPane { multipleSelectionSupport: false, setRowLineHeight: false, horizontalScrolling: false, - accessibilityProvider: >{ + accessibilityProvider: { getAriaLabel(extension: IExtension | null): string { return getAriaLabelForExtension(extension); }, diff --git a/src/vs/workbench/contrib/extensions/browser/keymapRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/keymapRecommendations.ts index 3ad131168e5..613fe523715 100644 --- a/src/vs/workbench/contrib/extensions/browser/keymapRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/keymapRecommendations.ts @@ -20,7 +20,7 @@ export class KeymapRecommendations extends ExtensionRecommendations { protected async doActivate(): Promise { if (this.productService.keymapExtensionTips) { - this._recommendations = this.productService.keymapExtensionTips.map(extensionId => ({ + this._recommendations = this.productService.keymapExtensionTips.map(extensionId => ({ extension: extensionId.toLowerCase(), reason: { reasonId: ExtensionRecommendationReason.Application, diff --git a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts index 0c5405c87b0..c12bb973c23 100644 --- a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts +++ b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts @@ -7,7 +7,7 @@ import { Disposable, DisposableStore, IDisposable, MutableDisposable } from 'vs/ import { isWeb } from 'vs/base/common/platform'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { localize, localize2 } from 'vs/nls'; -import { Action2, IMenuService, ISubmenuItem, MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; +import { Action2, IMenuService, MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; @@ -92,7 +92,7 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements const getProfilesTitle = () => { return localize('profiles', "Profiles ({0})", this.userDataProfileService.currentProfile.name); }; - MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { + MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { get title() { return getProfilesTitle(); }, @@ -100,7 +100,7 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements group: '2_configuration', order: 1, }); - MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { + MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { get title() { return getProfilesTitle(); }, diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncConflictsView.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncConflictsView.ts index 37c0a56e082..890ffcf50db 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncConflictsView.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncConflictsView.ts @@ -92,7 +92,7 @@ export class UserDataSyncConflictsViewPane extends TreeViewPane implements IUser label: { label: basename(resource.remoteResource), strikethrough: resource.mergeState === MergeState.Accepted && (resource.localChange === Change.Deleted || resource.remoteChange === Change.Deleted) }, description: getSyncAreaLabel(resource.syncResource), collapsibleState: TreeItemCollapsibleState.None, - command: { id: `workbench.actions.sync.openConflicts`, title: '', arguments: [{ $treeViewId: '', $treeItemHandle: handle }] }, + command: { id: `workbench.actions.sync.openConflicts`, title: '', arguments: [{ $treeViewId: '', $treeItemHandle: handle } satisfies TreeViewItemHandleArg] }, contextValue: `sync-conflict-resource` }; children.push(treeItem); diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts index 34da3b15c2a..22e8f21e168 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts @@ -61,7 +61,7 @@ export class UserDataSyncDataViews extends Disposable { private registerConflictsView(container: ViewContainer): void { const viewsRegistry = Registry.as(Extensions.ViewsRegistry); const viewName = localize2('conflicts', "Conflicts"); - viewsRegistry.registerViews([{ + const viewDescriptor: ITreeViewDescriptor = { id: SYNC_CONFLICTS_VIEW_ID, name: viewName, ctorDescriptor: new SyncDescriptor(UserDataSyncConflictsViewPane), @@ -71,7 +71,8 @@ export class UserDataSyncDataViews extends Disposable { treeView: this.instantiationService.createInstance(TreeView, SYNC_CONFLICTS_VIEW_ID, viewName.value), collapsed: false, order: 100, - }], container); + }; + viewsRegistry.registerViews([viewDescriptor], container); } private registerMachinesView(container: ViewContainer): void { @@ -85,7 +86,7 @@ export class UserDataSyncDataViews extends Disposable { this._register(Event.any(this.userDataSyncMachinesService.onDidChange, this.userDataSyncService.onDidResetRemote)(() => treeView.refresh())); const viewsRegistry = Registry.as(Extensions.ViewsRegistry); - viewsRegistry.registerViews([{ + const viewDescriptor: ITreeViewDescriptor = { id, name, ctorDescriptor: new SyncDescriptor(TreeViewPane), @@ -95,7 +96,8 @@ export class UserDataSyncDataViews extends Disposable { treeView, collapsed: false, order: 300, - }], container); + }; + viewsRegistry.registerViews([viewDescriptor], container); this._register(registerAction2(class extends Action2 { constructor() { @@ -152,7 +154,7 @@ export class UserDataSyncDataViews extends Disposable { this.userDataSyncService.onDidResetLocal, this.userDataSyncService.onDidResetRemote)(() => treeView.refresh())); const viewsRegistry = Registry.as(Extensions.ViewsRegistry); - viewsRegistry.registerViews([{ + const viewDescriptor: ITreeViewDescriptor = { id, name, ctorDescriptor: new SyncDescriptor(TreeViewPane), @@ -163,7 +165,8 @@ export class UserDataSyncDataViews extends Disposable { collapsed: false, order: remote ? 200 : 400, hideByDefault: !remote, - }], container); + }; + viewsRegistry.registerViews([viewDescriptor], container); this.registerDataViewActions(id); } @@ -178,7 +181,7 @@ export class UserDataSyncDataViews extends Disposable { treeView.dataProvider = dataProvider; const viewsRegistry = Registry.as(Extensions.ViewsRegistry); - viewsRegistry.registerViews([{ + const viewDescriptor: ITreeViewDescriptor = { id, name, ctorDescriptor: new SyncDescriptor(TreeViewPane), @@ -188,7 +191,8 @@ export class UserDataSyncDataViews extends Disposable { treeView, collapsed: false, hideByDefault: false, - }], container); + }; + viewsRegistry.registerViews([viewDescriptor], container); this._register(registerAction2(class extends Action2 { constructor() { @@ -303,7 +307,7 @@ export class UserDataSyncDataViews extends Disposable { treeView.dataProvider = dataProvider; const viewsRegistry = Registry.as(Extensions.ViewsRegistry); - viewsRegistry.registerViews([{ + const viewDescriptor: ITreeViewDescriptor = { id, name, ctorDescriptor: new SyncDescriptor(TreeViewPane), @@ -314,7 +318,8 @@ export class UserDataSyncDataViews extends Disposable { collapsed: false, order: 500, hideByDefault: true - }], container); + }; + viewsRegistry.registerViews([viewDescriptor], container); } From 243494c1785a99ad714d3ce67e6e5270c4afe8e6 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Wed, 15 May 2024 11:38:47 +0200 Subject: [PATCH 194/357] set UV_USE_IO_URING in code server for linux (#212791) --- resources/server/bin/code-server-linux.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/server/bin/code-server-linux.sh b/resources/server/bin/code-server-linux.sh index 3df32dfd43c..3d8881ee601 100644 --- a/resources/server/bin/code-server-linux.sh +++ b/resources/server/bin/code-server-linux.sh @@ -9,4 +9,6 @@ esac ROOT="$(dirname "$(dirname "$(readlink -f "$0")")")" +export UV_USE_IO_URING=0 # workaround for https://github.com/microsoft/vscode/issues/212678 + "$ROOT/node" ${INSPECT:-} "$ROOT/out/server-main.js" "$@" From 942d81c5b1cbfbb21ce3539bc126e1834cceb621 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Wed, 15 May 2024 12:41:13 +0200 Subject: [PATCH 195/357] Improve tabs multi select theme colors and high contrast support (#212795) Improved tabs multi select theme colors and high contrast support --- extensions/theme-defaults/themes/dark_modern.json | 1 + extensions/theme-defaults/themes/dark_vs.json | 2 ++ extensions/theme-defaults/themes/light_modern.json | 3 ++- extensions/theme-defaults/themes/light_vs.json | 1 + .../workbench/browser/parts/editor/multiEditorTabsControl.ts | 5 +++++ 5 files changed, 11 insertions(+), 1 deletion(-) diff --git a/extensions/theme-defaults/themes/dark_modern.json b/extensions/theme-defaults/themes/dark_modern.json index d6703a73704..57578ca9692 100644 --- a/extensions/theme-defaults/themes/dark_modern.json +++ b/extensions/theme-defaults/themes/dark_modern.json @@ -100,6 +100,7 @@ "tab.activeBorder": "#1F1F1F", "tab.activeBorderTop": "#0078D4", "tab.activeForeground": "#FFFFFF", + "tab.selectedBorderTop": "#6caddf", "tab.border": "#2B2B2B", "tab.hoverBackground": "#1F1F1F", "tab.inactiveBackground": "#181818", diff --git a/extensions/theme-defaults/themes/dark_vs.json b/extensions/theme-defaults/themes/dark_vs.json index 543e4efcecd..331a87bb778 100644 --- a/extensions/theme-defaults/themes/dark_vs.json +++ b/extensions/theme-defaults/themes/dark_vs.json @@ -22,6 +22,8 @@ "ports.iconRunningProcessForeground": "#369432", "sideBarSectionHeader.background": "#0000", "sideBarSectionHeader.border": "#ccc3", + "tab.selectedBackground": "#222222", + "tab.selectedForeground": "#ffffffa0", "tab.lastPinnedBorder": "#ccc3", "list.activeSelectionIconForeground": "#FFF", "terminal.inactiveSelectionBackground": "#3A3D41", diff --git a/extensions/theme-defaults/themes/light_modern.json b/extensions/theme-defaults/themes/light_modern.json index d39d41eee22..bd7e647afb3 100644 --- a/extensions/theme-defaults/themes/light_modern.json +++ b/extensions/theme-defaults/themes/light_modern.json @@ -116,6 +116,7 @@ "tab.activeBorder": "#F8F8F8", "tab.activeBorderTop": "#005FB8", "tab.activeForeground": "#3B3B3B", + "tab.selectedBorderTop": "#68a3da", "tab.border": "#E5E5E5", "tab.hoverBackground": "#FFFFFF", "tab.inactiveBackground": "#F8F8F8", @@ -134,7 +135,7 @@ "textLink.activeForeground": "#005FB8", "textLink.foreground": "#005FB8", "textPreformat.foreground": "#3B3B3B", - "textPreformat.background": "#0000001F", + "textPreformat.background": "#0000001F", "textSeparator.foreground": "#21262D", "titleBar.activeBackground": "#F8F8F8", "titleBar.activeForeground": "#1E1E1E", diff --git a/extensions/theme-defaults/themes/light_vs.json b/extensions/theme-defaults/themes/light_vs.json index bdd063fbc56..50a315348f7 100644 --- a/extensions/theme-defaults/themes/light_vs.json +++ b/extensions/theme-defaults/themes/light_vs.json @@ -23,6 +23,7 @@ "ports.iconRunningProcessForeground": "#369432", "sideBarSectionHeader.background": "#0000", "sideBarSectionHeader.border": "#61616130", + "tab.selectedForeground": "#333333b3", "tab.lastPinnedBorder": "#61616130", "notebook.cellBorderColor": "#E8E8E8", "notebook.selectedCellBackground": "#c8ddf150", diff --git a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts index 45b24ac116c..3c8e21480e8 100644 --- a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts @@ -2272,6 +2272,11 @@ registerThemingParticipant((theme, collector) => { outline-offset: -5px; } + .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.selected:not(.active):not(:hover) { + outline: 1px dotted; + outline-offset: -5px; + } + .monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.active:focus { outline-style: dashed; } From 8daa0c10bfe8ca3c88f62e110733c3f9c3ebd740 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 15 May 2024 13:09:02 +0200 Subject: [PATCH 196/357] voice - render markdown as string before synthesizing (#212796) --- .../actions/voiceChatActions.ts | 86 +++++++++++-------- .../electron-sandbox/voiceChatActions.test.ts | 44 ++++++++++ 2 files changed, 95 insertions(+), 35 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/test/electron-sandbox/voiceChatActions.test.ts diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts index e0a21d0ccb8..5e4802353de 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts @@ -45,7 +45,7 @@ import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/com import { InlineChatController } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; -import { HasSpeechProvider, ISpeechService, ITextToSpeechSession, KeywordRecognitionStatus, SpeechToTextInProgress, SpeechToTextStatus, TextToSpeechStatus, TextToSpeechInProgress as GlobalTextToSpeechInProgress } from 'vs/workbench/contrib/speech/common/speechService'; +import { HasSpeechProvider, ISpeechService, KeywordRecognitionStatus, SpeechToTextInProgress, SpeechToTextStatus, TextToSpeechStatus, TextToSpeechInProgress as GlobalTextToSpeechInProgress } from 'vs/workbench/contrib/speech/common/speechService'; import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalChatContextKeys, TerminalChatController } from 'vs/workbench/contrib/terminal/browser/terminalContribExports'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -55,6 +55,7 @@ import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, StatusbarA import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; import { IChatResponseModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; +import { renderStringAsPlaintext } from 'vs/base/browser/markdownRenderer'; //#region Speech to Text @@ -749,24 +750,12 @@ class ChatSynthesizerSessions { } })); - if (controller.response.isComplete) { - return this.synthesizeCompletedResponse(session, controller.response); - } else { - return this.synthesizePendingResponse(session, controller.response, activeSession.token); - } - } - - private synthesizeCompletedResponse(session: ITextToSpeechSession, response: IChatResponseModel): Promise { - return session.synthesize(response.response.asString()); - } - - private async synthesizePendingResponse(session: ITextToSpeechSession, response: IChatResponseModel, token: CancellationToken): Promise { - for await (const chunk of this.nextChatResponseChunk(response, token)) { - if (token.isCancellationRequested) { + for await (const chunk of this.nextChatResponseChunk(controller.response, activeSession.token)) { + if (activeSession.token.isCancellationRequested) { return; } - await raceCancellation(session.synthesize(chunk), token); + await raceCancellation(session.synthesize(chunk), activeSession.token); } } @@ -774,39 +763,43 @@ class ChatSynthesizerSessions { let totalOffset = 0; let complete = false; do { - const text = response.response.asString(); - const { chunks, offset, tail } = this.toChunks(text, totalOffset); + const responseLength = response.response.asString().length; + const { chunk, offset } = this.parseNextChatResponseChunk(response, totalOffset); totalOffset = offset; complete = response.isComplete; - for (const chunk of chunks) { + if (chunk) { yield chunk; - - if (token.isCancellationRequested) { - return; - } } - if (complete) { - yield tail; - } else if (text === response.response.asString()) { + if (token.isCancellationRequested) { + return; + } + + if (!complete && responseLength === response.response.asString().length) { await raceCancellation(Event.toPromise(response.onDidChange), token); // wait for the response to change } } while (!token.isCancellationRequested && !complete); } - private toChunks(text: string, offset: number): { readonly chunks: string[]; readonly offset: number; readonly tail: string } { - const chunks: string[] = []; + private parseNextChatResponseChunk(response: IChatResponseModel, offset: number): { readonly chunk: string | undefined; readonly offset: number } { + let chunk: string | undefined = undefined; - for (let i = offset; i < text.length; i++) { - const char = text[i]; - if (char === '.' || char === '!' || char === '?' || char === ':') { - chunks.push(text.substring(offset, i + 1)); - offset = i + 1; - } + const text = response.response.asString(); + + if (response.isComplete) { + chunk = text.substring(offset); + offset = text.length + 1; + } else { + const res = parseNextChatResponseChunk(text, offset); + chunk = res.chunk; + offset = res.offset; } - return { chunks, offset, tail: text.substring(offset) }; + return { + chunk: chunk ? renderStringAsPlaintext({ value: chunk }) : chunk, // convert markdown to plain text + offset + }; } stop(): void { @@ -815,6 +808,29 @@ class ChatSynthesizerSessions { } } +const sentenceDelimiter = ['.', '!', '?', ':']; +const lineDelimiter = '\n'; +const wordDelimiter = ' '; + +export function parseNextChatResponseChunk(text: string, offset: number): { readonly chunk: string | undefined; readonly offset: number } { + let chunk: string | undefined = undefined; + + for (let i = text.length - 1; i >= offset; i--) { // going from end to start to produce largest chunks + const cur = text[i]; + const next = text[i + 1]; + if ( + sentenceDelimiter.includes(cur) && next === wordDelimiter || // end of sentence + lineDelimiter === cur // end of line + ) { + chunk = text.substring(offset, i + 1).trim(); + offset = i + 1; + break; + } + } + + return { chunk, offset }; +} + export class ReadChatResponseAloud extends Action2 { constructor() { super({ diff --git a/src/vs/workbench/contrib/chat/test/electron-sandbox/voiceChatActions.test.ts b/src/vs/workbench/contrib/chat/test/electron-sandbox/voiceChatActions.test.ts new file mode 100644 index 00000000000..249fd8e457c --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/electron-sandbox/voiceChatActions.test.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { parseNextChatResponseChunk } from 'vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions'; + +suite('VoiceChatActions', function () { + + function assertChunk(text: string, expected: string | undefined, offset: number): { chunk: string | undefined; offset: number } { + const res = parseNextChatResponseChunk(text, offset); + assert.strictEqual(res.chunk, expected); + + return res; + } + + test('parseNextChatResponseChunk', function () { + + // Simple, no offset + assertChunk('Hello World', undefined, 0); + assertChunk('Hello World.', undefined, 0); + assertChunk('Hello World. ', 'Hello World.', 0); + assertChunk('Hello World? ', 'Hello World?', 0); + assertChunk('Hello World! ', 'Hello World!', 0); + assertChunk('Hello World: ', 'Hello World:', 0); + + // Ensure chunks are parsed from the end, no offset + assertChunk('Hello World. How is your day? And more...', 'Hello World. How is your day?', 0); + + // Ensure chunks are parsed from the end, with offset + let offset = assertChunk('Hello World. How is your ', 'Hello World.', 0).offset; + offset = assertChunk('Hello World. How is your day? And more...', 'How is your day?', offset).offset; + offset = assertChunk('Hello World. How is your day? And more to come! ', 'And more to come!', offset).offset; + assertChunk('Hello World. How is your day? And more to come! ', undefined, offset); + + // Sparted by newlines + offset = assertChunk('Hello World.\nHow is your', 'Hello World.', 0).offset; + assertChunk('Hello World.\nHow is your day?\n', 'How is your day?', offset); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); +}); From 6317965640a96ca6054b63522a9c4760bfa69054 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 15 May 2024 13:10:05 +0200 Subject: [PATCH 197/357] shared process - show notification toast on crash (fix #212103) (#212799) --- src/vs/code/electron-main/app.ts | 2 ++ .../sharedProcess/electron-main/sharedProcess.ts | 6 ++++++ src/vs/workbench/electron-sandbox/window.ts | 12 ++++++++++++ 3 files changed, 20 insertions(+) diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index f0f6dc535a4..d19e8bfe49b 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -989,6 +989,8 @@ export class CodeApplication extends Disposable { private setupSharedProcess(machineId: string, sqmId: string): { sharedProcessReady: Promise; sharedProcessClient: Promise } { const sharedProcess = this._register(this.mainInstantiationService.createInstance(SharedProcess, machineId, sqmId)); + this._register(sharedProcess.onDidCrash(() => this.windowsMainService?.sendToFocused('vscode:reportSharedProcessCrash'))); + const sharedProcessClient = (async () => { this.logService.trace('Main->SharedProcess#connect'); diff --git a/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts b/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts index 087f5858b48..21038e9dc86 100644 --- a/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts +++ b/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts @@ -19,6 +19,7 @@ import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtil import { parseSharedProcessDebugPort } from 'vs/platform/environment/node/environmentService'; import { assertIsDefined } from 'vs/base/common/types'; import { SharedProcessChannelConnection, SharedProcessRawConnection, SharedProcessLifecycle } from 'vs/platform/sharedProcess/common/sharedProcess'; +import { Emitter } from 'vs/base/common/event'; export class SharedProcess extends Disposable { @@ -27,6 +28,9 @@ export class SharedProcess extends Disposable { private utilityProcess: UtilityProcess | undefined = undefined; private utilityProcessLogListener: IDisposable | undefined = undefined; + private readonly _onDidCrash = this._register(new Emitter()); + readonly onDidCrash = this._onDidCrash.event; + constructor( private readonly machineId: string, private readonly sqmId: string, @@ -168,6 +172,8 @@ export class SharedProcess extends Disposable { payload: this.createSharedProcessConfiguration(), execArgv }); + + this._register(this.utilityProcess.onCrash(() => this._onDidCrash.fire())); } private createSharedProcessConfiguration(): ISharedProcessConfiguration { diff --git a/src/vs/workbench/electron-sandbox/window.ts b/src/vs/workbench/electron-sandbox/window.ts index 419b0614a76..73ebdb1b2a9 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -196,6 +196,18 @@ export class NativeWindow extends BaseWindow { } }); + // Shared Process crash reported from main + ipcRenderer.on('vscode:reportSharedProcessCrash', (event: unknown, error: string) => { + this.notificationService.prompt( + Severity.Error, + localize('sharedProcessCrash', "A shared background process terminated unexpectedly. Please restart the application to recover."), + [{ + label: localize('restart', "Restart"), + run: () => this.nativeHostService.relaunch() + }] + ); + }); + // Support openFiles event for existing and new files ipcRenderer.on('vscode:openFiles', (event: unknown, request: IOpenFileRequest) => { this.onOpenFiles(request); }); From d0aceadf96818370373b5337f581580e11339a99 Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Wed, 15 May 2024 13:01:48 +0200 Subject: [PATCH 198/357] Update distro hash --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 5cc7a43e090..6b0467b4a14 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.90.0", - "distro": "26ff176dca24e02834457f98dd01bb4513dda29d", + "distro": "b885c5b015796a5b6373decb919a391522135903", "author": { "name": "Microsoft Corporation" }, @@ -229,4 +229,4 @@ "optionalDependencies": { "windows-foreground-love": "0.5.0" } -} \ No newline at end of file +} From f0676fb01a08c6d2f3f5e8f189488b4f0f3f667a Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Wed, 15 May 2024 14:13:21 +0200 Subject: [PATCH 199/357] Fixes #211742 (#212807) --- .../browser/inlineCompletionsHintsWidget.ts | 49 ++++++++++++------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget.ts index 00c79719484..9f7d3d17ba8 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget.ts @@ -11,7 +11,8 @@ import { equals } from 'vs/base/common/arrays'; import { RunOnceScheduler } from 'vs/base/common/async'; import { Codicon } from 'vs/base/common/codicons'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; -import { IObservable, autorun, autorunWithStore, derived, observableFromEvent } from 'vs/base/common/observable'; +import { IObservable, autorun, autorunWithStore, derived, derivedObservableWithCache, observableFromEvent } from 'vs/base/common/observable'; +import { derivedWithStore } from 'vs/base/common/observableInternal/derived'; import { OS } from 'vs/base/common/platform'; import { ThemeIcon } from 'vs/base/common/themables'; import 'vs/css!./inlineCompletionsHintsWidget'; @@ -71,26 +72,36 @@ export class InlineCompletionsHintsWidget extends Disposable { return; } - const contentWidget = store.add(this.instantiationService.createInstance( - InlineSuggestionHintsContentWidget, - this.editor, - true, - this.position, - model.selectedInlineCompletionIndex, - model.inlineCompletionsCount, - model.activeCommands, - )); - editor.addContentWidget(contentWidget); - store.add(toDisposable(() => editor.removeContentWidget(contentWidget))); + const contentWidgetValue = derivedWithStore((reader, store) => { + const contentWidget = store.add(this.instantiationService.createInstance( + InlineSuggestionHintsContentWidget, + this.editor, + true, + this.position, + model.selectedInlineCompletionIndex, + model.inlineCompletionsCount, + model.activeCommands, + )); + editor.addContentWidget(contentWidget); + store.add(toDisposable(() => editor.removeContentWidget(contentWidget))); + store.add(autorun(reader => { + /** @description request explicit */ + const position = this.position.read(reader); + if (!position) { + return; + } + if (model.lastTriggerKind.read(reader) !== InlineCompletionTriggerKind.Explicit) { + model.triggerExplicitly(); + } + })); + return contentWidget; + }); + + const hadPosition = derivedObservableWithCache(this, (reader, lastValue) => !!this.position.read(reader) || !!lastValue); store.add(autorun(reader => { - /** @description request explicit */ - const position = this.position.read(reader); - if (!position) { - return; - } - if (model.lastTriggerKind.read(reader) !== InlineCompletionTriggerKind.Explicit) { - model.triggerExplicitly(); + if (hadPosition.read(reader)) { + contentWidgetValue.read(reader); } })); })); From 5d52fe6d545cfa367a72450594d5f8eaf706b36f Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 15 May 2024 15:11:38 +0200 Subject: [PATCH 200/357] rename `LanguageModelChatResponse#stream` to `text` (#212811) https://github.com/microsoft/vscode/issues/206265 --- src/vs/workbench/api/common/extHostLanguageModels.ts | 2 +- src/vscode-dts/vscode.proposed.languageModels.d.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/api/common/extHostLanguageModels.ts b/src/vs/workbench/api/common/extHostLanguageModels.ts index a97ec1648ba..09336569a75 100644 --- a/src/vs/workbench/api/common/extHostLanguageModels.ts +++ b/src/vs/workbench/api/common/extHostLanguageModels.ts @@ -60,7 +60,7 @@ class LanguageModelResponse { const that = this; this.apiObject = { // result: promise, - stream: that._defaultStream.asyncIterable, + text: that._defaultStream.asyncIterable, // streams: AsyncIterable[] // FUTURE responses per N }; } diff --git a/src/vscode-dts/vscode.proposed.languageModels.d.ts b/src/vscode-dts/vscode.proposed.languageModels.d.ts index f2614f8709e..b95804245da 100644 --- a/src/vscode-dts/vscode.proposed.languageModels.d.ts +++ b/src/vscode-dts/vscode.proposed.languageModels.d.ts @@ -100,8 +100,7 @@ declare module 'vscode' { * To cancel the stream, the consumer can {@link CancellationTokenSource.cancel cancel} the token that was used to make the request * or break from the for-loop. */ - // TODO@API rename: text - stream: AsyncIterable; + text: AsyncIterable; } /** From ba6864cbdd0c1c535e4ec1e075caf15024ac989e Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 15 May 2024 17:13:14 +0200 Subject: [PATCH 201/357] refactor: improve handling of chat widget context in voice chat actions (#212815) --- .../actions/voiceChatActions.ts | 44 ++++++++++++++----- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts index 5e4802353de..3a16b271500 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts @@ -86,6 +86,7 @@ interface IVoiceChatSessionController { readonly onDidHideInput: Event; readonly context: VoiceChatSessionContext; + readonly scopedContextKeyService: IContextKeyService; updateState(state: VoiceChatSessionState): void; @@ -204,6 +205,7 @@ class VoiceChatSessionControllerFactory { private static doCreateForChatWidget(context: VoiceChatSessionContext, chatWidget: IChatWidget): IVoiceChatSessionController { return { context, + scopedContextKeyService: chatWidget.scopedContextKeyService, onDidAcceptInput: chatWidget.onDidAcceptInput, onDidHideInput: chatWidget.onDidHide, focusInput: () => chatWidget.focusInput(), @@ -220,6 +222,7 @@ class VoiceChatSessionControllerFactory { const context = 'terminal'; return { context, + scopedContextKeyService: terminalChat.scopedContextKeyService, onDidAcceptInput: terminalChat.onDidAcceptInput, onDidHideInput: terminalChat.onDidHide, focusInput: () => terminalChat.focus(), @@ -380,7 +383,8 @@ class VoiceChatSessions { return; } - const response = await this.currentVoiceChatSession.controller.acceptInput(); + const controller = this.currentVoiceChatSession.controller; + const response = await controller.acceptInput(); if (!response) { return; } @@ -389,8 +393,16 @@ class VoiceChatSessions { !this.accessibilityService.isScreenReaderOptimized() && // do not auto synthesize when screen reader is active this.configurationService.getValue(AccessibilityVoiceSettingId.AutoSynthesize) === true ) { - const controller = this.instantiationService.invokeFunction(accessor => ChatSynthesizerSessionController.create(accessor, response)); - ChatSynthesizerSessions.getInstance(this.instantiationService).start(controller); + let context: IVoiceChatSessionController | 'focused'; + if (controller.context === 'inline') { + // TODO@bpasero this is ugly, but the lightweight inline chat turns into + // a different widget as soon as a response comes in, so we fallback to + // picking up from the focused chat widget + context = 'focused'; + } else { + context = controller; + } + ChatSynthesizerSessions.getInstance(this.instantiationService).start(this.instantiationService.invokeFunction(accessor => ChatSynthesizerSessionController.create(accessor, context, response))); } } } @@ -680,20 +692,28 @@ interface IChatSynthesizerSessionController { class ChatSynthesizerSessionController { - static create(accessor: ServicesAccessor, response: IChatResponseModel): IChatSynthesizerSessionController { + static create(accessor: ServicesAccessor, context: IVoiceChatSessionController | 'focused', response: IChatResponseModel): IChatSynthesizerSessionController { const chatWidgetService = accessor.get(IChatWidgetService); const contextKeyService = accessor.get(IContextKeyService); - let chatWidget = chatWidgetService.getWidgetBySessionId(response.session.sessionId); - if (chatWidget?.location === ChatAgentLocation.Editor) { - // somehow for inline chat, the response session returns the wrong widget - // so we need to find the correct one by going through the last focused - chatWidget = chatWidgetService.lastFocusedWidget; + if (context === 'focused') { + let chatWidget = chatWidgetService.getWidgetBySessionId(response.session.sessionId); + if (chatWidget?.location === ChatAgentLocation.Editor) { + // TODO@bpasero workaround for https://github.com/microsoft/vscode/issues/212785 + // but should find a better way how to get to the chat widget from a response + chatWidget = chatWidgetService.lastFocusedWidget; + } + + return { + onDidHideChat: chatWidget?.onDidHide ?? Event.None, + contextKeyService: chatWidget?.scopedContextKeyService ?? contextKeyService, + response + }; } return { - onDidHideChat: chatWidget?.onDidHide ?? Event.None, - contextKeyService: chatWidget?.scopedContextKeyService ?? contextKeyService, + onDidHideChat: context.onDidHideInput, + contextKeyService: context.scopedContextKeyService, response }; } @@ -859,7 +879,7 @@ export class ReadChatResponseAloud extends Action2 { return; } - const controller = ChatSynthesizerSessionController.create(accessor, response.model); + const controller = ChatSynthesizerSessionController.create(accessor, 'focused', response.model); ChatSynthesizerSessions.getInstance(instantiationService).start(controller); } } From bd653ce6c9856feeadf7d41c0f0f9879b388aa4c Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 15 May 2024 17:49:33 +0200 Subject: [PATCH 202/357] move todos into issue (#212821) https://github.com/microsoft/vscode/issues/206265 --- src/vscode-dts/vscode.proposed.languageModels.d.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/vscode-dts/vscode.proposed.languageModels.d.ts b/src/vscode-dts/vscode.proposed.languageModels.d.ts index b95804245da..2024eb9fda4 100644 --- a/src/vscode-dts/vscode.proposed.languageModels.d.ts +++ b/src/vscode-dts/vscode.proposed.languageModels.d.ts @@ -73,8 +73,6 @@ declare module 'vscode' { * * @see {@link LanguageModelAccess.chatRequest} */ - // TODO@API add something like `modelResult: Thenable<{ [name: string]: any }>` - // TODO@API: add a StopReason-enum that's also used in LanguageModelChat export interface LanguageModelChatResponse { /** From b8881656c6dafccbfe5cda474960c3cdc0d0dc4a Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 15 May 2024 18:11:37 +0200 Subject: [PATCH 203/357] fix #208324 (#212822) --- .../common/userDataSyncService.ts | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index eba3ad14980..856003580d0 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { equals } from 'vs/base/common/arrays'; -import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; +import { CancelablePromise, createCancelablePromise, RunOnceScheduler } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Emitter, Event } from 'vs/base/common/event'; @@ -98,6 +98,8 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ this._status = userDataSyncStoreManagementService.userDataSyncStore ? SyncStatus.Idle : SyncStatus.Uninitialized; this._lastSyncTime = this.storageService.getNumber(LAST_SYNC_TIME_KEY, StorageScope.APPLICATION, undefined); this._register(toDisposable(() => this.clearActiveProfileSynchronizers())); + + this._register(new RunOnceScheduler(() => this.cleanUpStaleStorageData(), 5 * 1000 /* after 5s */)).schedule(); } async createSyncTask(manifest: IUserDataManifest | null, disableCache?: boolean): Promise { @@ -245,6 +247,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ if (this.userDataProfilesService.profiles.some(p => p.id === profileSynchronizerItem[0].profile.id)) { continue; } + await profileSynchronizerItem[0].resetLocal(); profileSynchronizerItem[1].dispose(); this.activeProfileSynchronizers.delete(key); } @@ -397,6 +400,46 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ this.logService.info('Did reset the local sync state.'); } + private async cleanUpStaleStorageData(): Promise { + const allKeys = this.storageService.keys(StorageScope.APPLICATION, StorageTarget.MACHINE); + const lastSyncProfileKeys: [string, string][] = []; + for (const key of allKeys) { + if (!key.endsWith('.lastSyncUserData')) { + continue; + } + const segments = key.split('.'); + if (segments.length === 3) { + lastSyncProfileKeys.push([key, segments[0]]); + } + } + if (!lastSyncProfileKeys.length) { + return; + } + + const disposables = new DisposableStore(); + + try { + let defaultProfileSynchronizer = this.activeProfileSynchronizers.get(this.userDataProfilesService.defaultProfile.id)?.[0]; + if (!defaultProfileSynchronizer) { + defaultProfileSynchronizer = disposables.add(this.instantiationService.createInstance(ProfileSynchronizer, this.userDataProfilesService.defaultProfile, undefined)); + } + const userDataProfileManifestSynchronizer = defaultProfileSynchronizer.enabled.find(s => s.resource === SyncResource.Profiles) as UserDataProfilesManifestSynchroniser; + if (!userDataProfileManifestSynchronizer) { + return; + } + const lastSyncedProfiles = await userDataProfileManifestSynchronizer.getLastSyncedProfiles(); + const lastSyncedCollections = lastSyncedProfiles?.map(p => p.collection) ?? []; + for (const [key, collection] of lastSyncProfileKeys) { + if (!lastSyncedCollections.includes(collection)) { + this.logService.info(`Removing last sync state for stale profile: ${collection}`); + this.storageService.remove(key, StorageScope.APPLICATION); + } + } + } finally { + disposables.dispose(); + } + } + async cleanUpRemoteData(): Promise { const remoteProfiles = await this.userDataSyncResourceProviderService.getRemoteSyncedProfiles(); const remoteProfileCollections = remoteProfiles.map(profile => profile.collection); From c9242053216cae5d8b80c5513b13ef8cc5a8c4b5 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Wed, 15 May 2024 18:39:03 +0200 Subject: [PATCH 204/357] Make List / Tree Hovers Focusable (#212818) * fixes #211951 * fix test * Support updatable hover --- src/vs/base/browser/ui/hover/hover.ts | 6 ++++ .../base/browser/ui/hover/hoverDelegate2.ts | 1 + .../services/hoverService/hoverService.ts | 28 ++++++++++++---- .../hoverService/updatableHoverWidget.ts | 2 +- .../hover/test/browser/nullHoverService.ts | 1 + .../workbench/browser/actions/listCommands.ts | 33 ++----------------- .../extensions/browser/extensionsWidgets.ts | 7 ++-- 7 files changed, 39 insertions(+), 39 deletions(-) diff --git a/src/vs/base/browser/ui/hover/hover.ts b/src/vs/base/browser/ui/hover/hover.ts index a0f9422ce05..f2b7582d7fa 100644 --- a/src/vs/base/browser/ui/hover/hover.ts +++ b/src/vs/base/browser/ui/hover/hover.ts @@ -43,6 +43,11 @@ export interface IHoverDelegate2 { // TODO: Change hoverDelegate arg to exclude the actual delegate and instead use the new options setupUpdatableHover(hoverDelegate: IHoverDelegate, htmlElement: HTMLElement, content: IUpdatableHoverContentOrFactory, options?: IUpdatableHoverOptions): IUpdatableHover; + + /** + * Shows the hover for the given element if one has been setup. + */ + triggerUpdatableHover(htmlElement: HTMLElement): void; } export interface IHoverWidget extends IDisposable { @@ -246,6 +251,7 @@ export type IUpdatableHoverContentOrFactory = IUpdatableHoverContent | (() => IU export interface IUpdatableHoverOptions { actions?: IHoverAction[]; linkHandler?(url: string): void; + trapFocus?: boolean; } export interface IUpdatableHover extends IDisposable { diff --git a/src/vs/base/browser/ui/hover/hoverDelegate2.ts b/src/vs/base/browser/ui/hover/hoverDelegate2.ts index 90c71d65a1d..13a379222c1 100644 --- a/src/vs/base/browser/ui/hover/hoverDelegate2.ts +++ b/src/vs/base/browser/ui/hover/hoverDelegate2.ts @@ -10,6 +10,7 @@ let baseHoverDelegate: IHoverDelegate2 = { hideHover: () => undefined, showAndFocusLastHover: () => undefined, setupUpdatableHover: () => null!, + triggerUpdatableHover: () => undefined }; /** diff --git a/src/vs/editor/browser/services/hoverService/hoverService.ts b/src/vs/editor/browser/services/hoverService/hoverService.ts index bd74d1b3d67..8c5e7319331 100644 --- a/src/vs/editor/browser/services/hoverService/hoverService.ts +++ b/src/vs/editor/browser/services/hoverService/hoverService.ts @@ -62,7 +62,9 @@ export class HoverService extends Disposable implements IHoverService { // HACK, remove this check when #189076 is fixed if (!skipLastFocusedUpdate) { if (trapFocus && activeElement) { - this._lastFocusedElementBeforeOpen = activeElement as HTMLElement; + if (!activeElement.classList.contains('monaco-hover')) { + this._lastFocusedElementBeforeOpen = activeElement as HTMLElement; + } } else { this._lastFocusedElementBeforeOpen = undefined; } @@ -187,6 +189,8 @@ export class HoverService extends Disposable implements IHoverService { } } + private readonly _existingHovers = new Map(); + // TODO: Investigate performance of this function. There seems to be a lot of content created // and thrown away on start up setupUpdatableHover(hoverDelegate: IHoverDelegate, htmlElement: HTMLElement, content: IUpdatableHoverContentOrFactory, options?: IUpdatableHoverOptions | undefined): IUpdatableHover { @@ -216,15 +220,13 @@ export class HoverService extends Disposable implements IHoverService { hoverDelegate.onDidHideHover?.(); hoverWidget = undefined; } - htmlElement.removeAttribute('custom-hover-active'); }; - const triggerShowHover = (delay: number, focus?: boolean, target?: IHoverDelegateTarget) => { + const triggerShowHover = (delay: number, focus?: boolean, target?: IHoverDelegateTarget, trapFocus?: boolean) => { return new TimeoutTimer(async () => { if (!hoverWidget || hoverWidget.isDisposed) { hoverWidget = new UpdatableHoverWidget(hoverDelegate, target || htmlElement, delay > 0); - await hoverWidget.update(typeof content === 'function' ? content() : content, focus, options); - htmlElement.setAttribute('custom-hover-active', 'true'); + await hoverWidget.update(typeof content === 'function' ? content() : content, focus, { ...options, trapFocus }); } }, delay); }; @@ -299,7 +301,7 @@ export class HoverService extends Disposable implements IHoverService { const hover: IUpdatableHover = { show: focus => { hideHover(false, true); // terminate a ongoing mouse over preparation - triggerShowHover(0, focus); // show hover immediately + triggerShowHover(0, focus, undefined, focus); // show hover immediately }, hide: () => { hideHover(true, true); @@ -309,6 +311,7 @@ export class HoverService extends Disposable implements IHoverService { await hoverWidget?.update(content, undefined, hoverOptions); }, dispose: () => { + this._existingHovers.delete(htmlElement); mouseOverDomEmitter.dispose(); mouseLeaveEmitter.dispose(); mouseDownEmitter.dispose(); @@ -317,8 +320,21 @@ export class HoverService extends Disposable implements IHoverService { hideHover(true, true); } }; + this._existingHovers.set(htmlElement, hover); return hover; } + + triggerUpdatableHover(target: HTMLElement): void { + const hover = this._existingHovers.get(target); + if (hover) { + hover.show(true); + } + } + + public override dispose(): void { + this._existingHovers.forEach(hover => hover.dispose()); + super.dispose(); + } } function getHoverOptionsIdentity(options: IHoverOptions | undefined): IHoverOptions | number | string | undefined { diff --git a/src/vs/editor/browser/services/hoverService/updatableHoverWidget.ts b/src/vs/editor/browser/services/hoverService/updatableHoverWidget.ts index 869e493c1f9..762b6626aff 100644 --- a/src/vs/editor/browser/services/hoverService/updatableHoverWidget.ts +++ b/src/vs/editor/browser/services/hoverService/updatableHoverWidget.ts @@ -42,7 +42,7 @@ export class UpdatableHoverWidget implements IDisposable { // show 'Loading' if no hover is up yet if (!this._hoverWidget) { - this.show(localize('iconLabel.loading', "Loading..."), focus); + this.show(localize('iconLabel.loading', "Loading..."), focus, options); } // compute the content diff --git a/src/vs/platform/hover/test/browser/nullHoverService.ts b/src/vs/platform/hover/test/browser/nullHoverService.ts index 1040ba4d772..6b7b728325b 100644 --- a/src/vs/platform/hover/test/browser/nullHoverService.ts +++ b/src/vs/platform/hover/test/browser/nullHoverService.ts @@ -12,4 +12,5 @@ export const NullHoverService: IHoverService = { showHover: () => undefined, setupUpdatableHover: () => Disposable.None as any, showAndFocusLastHover: () => undefined, + triggerUpdatableHover: () => undefined }; diff --git a/src/vs/workbench/browser/actions/listCommands.ts b/src/vs/workbench/browser/actions/listCommands.ts index 12e356a7ea7..01de60e8080 100644 --- a/src/vs/workbench/browser/actions/listCommands.ts +++ b/src/vs/workbench/browser/actions/listCommands.ts @@ -18,11 +18,11 @@ import { ITreeNode } from 'vs/base/browser/ui/tree/tree'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { Table } from 'vs/base/browser/ui/table/tableWidget'; import { AbstractTree, TreeFindMatchType, TreeFindMode } from 'vs/base/browser/ui/tree/abstractTree'; -import { EventType, getActiveWindow, isActiveElement } from 'vs/base/browser/dom'; +import { isActiveElement } from 'vs/base/browser/dom'; import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { localize, localize2 } from 'vs/nls'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; function ensureDOMFocus(widget: ListWidget | undefined): void { // it can happen that one of the commands is executed while @@ -60,10 +60,6 @@ async function navigate(widget: WorkbenchListWidget | undefined, updateFocusFn: return; } - if (activeHover) { - toggleCustomHover(activeHover, widget); - } - await updateFocus(widget, updateFocusFn); const listFocus = widget.getFocus(); @@ -733,33 +729,10 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ return; } - toggleCustomHover(elementWithHover as HTMLElement, lastFocusedList); + accessor.get(IHoverService).triggerUpdatableHover(elementWithHover as HTMLElement); }, }); -let activeHover: undefined | HTMLElement; -let disposable: IDisposable | undefined; -function toggleCustomHover(element: HTMLElement, list: WorkbenchListWidget) { - const show = !element.getAttribute('custom-hover-active'); - const mouseEvent = new MouseEvent(show ? EventType.MOUSE_OVER : EventType.MOUSE_LEAVE, { - view: getActiveWindow(), - bubbles: true, - cancelable: true, - }); - element.dispatchEvent(mouseEvent); - - if (activeHover === element && !show) { - activeHover = undefined; - disposable?.dispose(); - disposable = undefined; - } else { - activeHover = element; - disposable = list.onDidBlur(() => { - toggleCustomHover(element, list); - }); - } -} - KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'list.toggleExpand', weight: KeybindingWeight.WorkbenchContrib, diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts index e94ab96d100..3d725460a20 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts @@ -553,7 +553,7 @@ export class ExtensionHoverWidget extends ExtensionWidget { if (this.extension) { this.hover.value = this.hoverService.setupUpdatableHover({ delay: this.configurationService.getValue('workbench.hover.delay'), - showHover: (options) => { + showHover: (options, focus) => { return this.hoverService.showHover({ ...options, additionalClasses: ['extension-hover'], @@ -561,7 +561,10 @@ export class ExtensionHoverWidget extends ExtensionWidget { hoverPosition: this.options.position(), forcePosition: true, }, - }); + persistence: { + hideOnKeyDown: true, + } + }, focus); }, placement: 'element' }, this.options.target, { markdown: () => Promise.resolve(this.getHoverMarkdown()), markdownNotSupportedFallback: undefined }); From 00891c279234288282e20065c60ada96d3819840 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Wed, 15 May 2024 18:47:47 +0200 Subject: [PATCH 205/357] Update editor actions on settings change (#212824) --- .../workbench/browser/parts/editor/multiEditorTabsControl.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts index 3c8e21480e8..a35f3f1aa49 100644 --- a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts @@ -732,6 +732,11 @@ export class MultiEditorTabsControl extends EditorTabsControl { this.updateTabsScrollbarSizing(); } + // Update editor actions + if (oldOptions.alwaysShowEditorActions !== newOptions.alwaysShowEditorActions) { + this.updateEditorActionsToolbar(); + } + // Update tabs sizing if ( oldOptions.tabSizingFixedMinWidth !== newOptions.tabSizingFixedMinWidth || From e206bcb92266325efde7b4d1f118314023664eff Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Wed, 15 May 2024 19:19:35 +0200 Subject: [PATCH 206/357] Fix multi-select behavior on macOS (#212825) macos multiselect cmd --- .../browser/parts/editor/multiEditorTabsControl.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts index a35f3f1aa49..e99641dc2c1 100644 --- a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts @@ -898,7 +898,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { anchor = this.groupView.activeEditor!; } this.selectEditorsBetween(editor, anchor); - } else if (e.ctrlKey) { + } else if ((e.ctrlKey && !isMacintosh) || (e.metaKey && isMacintosh)) { if (this.groupView.isSelected(editor)) { this.groupView.unSelectEditor(editor); } else { @@ -949,8 +949,8 @@ export class MultiEditorTabsControl extends EditorTabsControl { if (this.originatesFromTabActionBar(e)) { return; // not when clicking on actions } - - if (!e.ctrlKey && !e.shiftKey && this.groupView.selectedEditors.length > 1) { + const isCtrlCmd = (e.ctrlKey && !isMacintosh) || (e.metaKey && isMacintosh); + if (!isCtrlCmd && !e.shiftKey && this.groupView.selectedEditors.length > 1) { this.unselectAllEditors(); } })); From f8ee0378c031513bb76376103e764cb4255a65e7 Mon Sep 17 00:00:00 2001 From: Miguel Medina Ballesteros Date: Wed, 15 May 2024 20:17:09 +0200 Subject: [PATCH 207/357] Add `AccessibilitySignal.terminalCommandSucceeded` and `success.mp3` (issue #178989) (#204430) --- .../browser/accessibilitySignalService.ts | 8 ++++++++ .../browser/accessibilityConfiguration.ts | 14 ++++++++++++++ .../terminal/browser/xterm/decorationAddon.ts | 2 ++ .../browser/terminal.accessibility.contribution.ts | 2 ++ 4 files changed, 26 insertions(+) diff --git a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts index 3ef15eb1b3a..36d1d1c3f9b 100644 --- a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts +++ b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts @@ -280,6 +280,7 @@ export class Sound { public static readonly error = Sound.register({ fileName: 'error.mp3' }); public static readonly warning = Sound.register({ fileName: 'warning.mp3' }); + public static readonly success = Sound.register({ fileName: 'success.mp3' }); public static readonly foldedArea = Sound.register({ fileName: 'foldedAreas.mp3' }); public static readonly break = Sound.register({ fileName: 'break.mp3' }); public static readonly quickFixes = Sound.register({ fileName: 'quickFixes.mp3' }); @@ -468,6 +469,13 @@ export class AccessibilitySignal { settingsKey: 'accessibility.signals.terminalCommandFailed', }); + public static readonly terminalCommandSucceeded = AccessibilitySignal.register({ + name: localize('accessibilitySignals.terminalCommandSucceeded', 'Terminal Command Succeeded'), + sound: Sound.success, + announcementMessage: localize('accessibility.signals.terminalCommandSucceeded', 'Command Succeeded'), + settingsKey: 'accessibility.signals.terminalCommandSucceeded', + }); + public static readonly terminalBell = AccessibilitySignal.register({ name: localize('accessibilitySignals.terminalBell', 'Terminal Bell'), sound: Sound.terminalBell, diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts index a9fef9c21f5..f5f1a703371 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts @@ -364,6 +364,20 @@ const configuration: IConfigurationNode = { }, } }, + 'accessibility.signals.terminalCommandSucceeded': { + ...signalFeatureBase, + 'description': localize('accessibility.signals.terminalCommandSucceeded', "Plays a signal - sound (audio cue) and/or announcement (alert) - when a terminal command succeeds (zero exit code) or when a command with such an exit code is navigated to in the accessible view."), + 'properties': { + 'sound': { + 'description': localize('accessibility.signals.terminalCommandSucceeded.sound', "Plays a sound when a terminal command succeeds (zero exit code) or when a command with such an exit code is navigated to in the accessible view."), + ...soundFeatureBase + }, + 'announcement': { + 'description': localize('accessibility.signals.terminalCommandSucceeded.announcement', "Announces when a terminal command succeeds (zero exit code) or when a command with such an exit code is navigated to in the accessible view."), + ...announcementFeatureBase + }, + } + }, 'accessibility.signals.terminalQuickFix': { ...signalFeatureBase, 'description': localize('accessibility.signals.terminalQuickFix', "Plays a signal - sound (audio cue) and/or announcement (alert) - when terminal Quick Fixes are available."), diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts b/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts index 11127e5226a..ed4925cadf6 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts @@ -230,6 +230,8 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { this.registerCommandDecoration(command); if (command.exitCode) { this._accessibilitySignalService.playSignal(AccessibilitySignal.terminalCommandFailed); + } else { + this._accessibilitySignalService.playSignal(AccessibilitySignal.terminalCommandSucceeded); } })); // Command invalidated diff --git a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts index 5233666f7e4..c2b8198873a 100644 --- a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts @@ -193,6 +193,8 @@ export class TerminalAccessibleViewContribution extends Disposable implements IT } if (command.exitCode) { this._accessibilitySignalService.playSignal(AccessibilitySignal.terminalCommandFailed); + } else { + this._accessibilitySignalService.playSignal(AccessibilitySignal.terminalCommandSucceeded); } } From c2baa46aa5b628e16641ecba0e66393fb88d96d4 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 15 May 2024 11:55:06 -0700 Subject: [PATCH 208/357] on blur of terminal chat, if no response content, hide widget (#212831) fix #212672 --- src/vs/workbench/contrib/terminal/browser/terminal.ts | 5 ----- .../workbench/contrib/terminal/browser/terminalInstance.ts | 1 - .../terminalContrib/chat/browser/terminalChatWidget.ts | 2 +- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 08162574c71..1dcaecd333a 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -639,11 +639,6 @@ export interface ITerminalInstance extends IBaseTerminalInstance { */ readonly isDisposed: boolean; - /** - * Whether this terminal is visible. - */ - readonly isVisible: boolean; - /** * Whether the terminal's pty is hosted on a remote. */ diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 5a7ff030371..e6ca453010d 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -266,7 +266,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { get isRemote(): boolean { return this._processManager.remoteAuthority !== undefined; } get remoteAuthority(): string | undefined { return this._processManager.remoteAuthority; } get hasFocus(): boolean { return dom.isAncestorOfActiveElement(this._wrapperElement); } - get isVisible(): boolean { return this._isVisible; } get title(): string { return this._title; } get titleSource(): TitleEventSource { return this._titleSource; } get icon(): TerminalIcon | undefined { return this._getIcon(); } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 0b4ec9dd073..735be4d4ec0 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -92,7 +92,7 @@ export class TerminalChatWidget extends Disposable { this._focusTracker = this._register(trackFocus(this._container)); this._register(this._focusTracker.onDidBlur(() => { - if (!this._instance.isVisible) { + if (!this.inlineChatWidget.responseContent) { this.hide(); } })); From afcb0ebb4eab7f180f5dbec986527e7225f6a61f Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 15 May 2024 14:21:47 -0700 Subject: [PATCH 209/357] Finalize chatParticipant and languageModels API (#212829) * Finalize chatParticipants proposal * Finalize languageModels API --- .../workbench/api/common/extHost.api.impl.ts | 3 - .../browser/chatParticipantContributions.ts | 30 +- .../common/extensionsApiProposals.ts | 2 - src/vscode-dts/vscode.d.ts | 854 ++++++++++++++++++ .../vscode.proposed.chatParticipant.d.ts | 505 ----------- .../vscode.proposed.languageModels.d.ts | 330 ------- 6 files changed, 855 insertions(+), 869 deletions(-) delete mode 100644 src/vscode-dts/vscode.proposed.chatParticipant.d.ts delete mode 100644 src/vscode-dts/vscode.proposed.languageModels.d.ts diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index ba39e1a01b4..64db6613bf3 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1420,7 +1420,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostLanguageFeatures.registerMappedEditsProvider(extension, selector, provider); }, createChatParticipant(id: string, handler: vscode.ChatExtendedRequestHandler) { - checkProposedApiEnabled(extension, 'chatParticipant'); return extHostChatAgents2.createChatAgent(extension, id, handler); }, createDynamicChatParticipant(id: string, dynamicProps: vscode.DynamicChatParticipantProps, handler: vscode.ChatExtendedRequestHandler): vscode.ChatParticipant { @@ -1432,11 +1431,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I // namespace: lm const lm: typeof vscode.lm = { selectChatModels: (selector) => { - checkProposedApiEnabled(extension, 'languageModels'); return extHostLanguageModels.selectLanguageModels(extension, selector ?? {}); }, onDidChangeChatModels: (listener, thisArgs?, disposables?) => { - checkProposedApiEnabled(extension, 'languageModels'); return extHostLanguageModels.onDidChangeProviders(listener, thisArgs, disposables); }, // --- embeddings diff --git a/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts b/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts index 373dc080b2f..d05ea9d07e5 100644 --- a/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts @@ -51,10 +51,6 @@ const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.regi description: localize('chatParticipantDescription', "A description of this chat participant, shown in the UI."), type: 'string' }, - isDefault: { - markdownDescription: localize('chatParticipantIsDefaultDescription', "**Only** allowed for extensions that have the `defaultChatParticipant` proposal."), - type: 'boolean', - }, isSticky: { description: localize('chatCommandSticky', "Whether invoking the command puts the chat into a persistent mode, where the command is automatically added to the chat input for the next message."), type: 'boolean' @@ -63,13 +59,6 @@ const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.regi description: localize('chatSampleRequest', "When the user clicks this participant in `/help`, this text will be submitted to the participant."), type: 'string' }, - defaultImplicitVariables: { - markdownDescription: '**Only** allowed for extensions that have the `chatParticipantAdditions` proposal. The names of the variables that are invoked by default', - type: 'array', - items: { - type: 'string' - } - }, commands: { markdownDescription: localize('chatCommandsDescription', "Commands available for this chat participant, which the user can invoke with a `/`."), type: 'array', @@ -99,26 +88,9 @@ const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.regi description: localize('chatCommandSticky', "Whether invoking the command puts the chat into a persistent mode, where the command is automatically added to the chat input for the next message."), type: 'boolean' }, - defaultImplicitVariables: { - markdownDescription: localize('defaultImplicitVariables', "**Only** allowed for extensions that have the `chatParticipantAdditions` proposal. The names of the variables that are invoked by default"), - type: 'array', - items: { - type: 'string' - } - }, } } }, - locations: { - markdownDescription: localize('chatLocationsDescription', "Locations in which this chat participant is available."), - type: 'array', - default: ['panel'], - items: { - type: 'string', - enum: ['panel', 'terminal', 'notebook'] - } - - } } } }, @@ -192,7 +164,7 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { continue; } - if (providerDescriptor.defaultImplicitVariables && !isProposedApiEnabled(extension.description, 'chatParticipantAdditions')) { + if ((providerDescriptor.defaultImplicitVariables || providerDescriptor.locations) && !isProposedApiEnabled(extension.description, 'chatParticipantAdditions')) { this.logService.error(`Extension '${extension.description.identifier.value}' CANNOT use API proposal: chatParticipantAdditions.`); continue; } diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index 19988575941..3dae0a8c70d 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -14,7 +14,6 @@ export const allApiProposals = Object.freeze({ authLearnMore: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authLearnMore.d.ts', authSession: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authSession.d.ts', canonicalUriProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.canonicalUriProvider.d.ts', - chatParticipant: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatParticipant.d.ts', chatParticipantAdditions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts', chatProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatProvider.d.ts', chatTab: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatTab.d.ts', @@ -76,7 +75,6 @@ export const allApiProposals = Object.freeze({ interactiveWindow: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.interactiveWindow.d.ts', ipc: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.ipc.d.ts', languageModelSystem: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.languageModelSystem.d.ts', - languageModels: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.languageModels.d.ts', languageStatusText: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.languageStatusText.d.ts', mappedEditsProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts', multiDocumentHighlightProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.multiDocumentHighlightProvider.d.ts', diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index f30cfa4a4eb..e33496f4bfe 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -7847,6 +7847,13 @@ declare module 'vscode' { * The current `Extension` instance. */ readonly extension: Extension; + + /** + * An object that keeps information about how this extension can use language models. + * + * @see {@link lm.sendChatRequest} + */ + readonly languageModelAccessInformation: LanguageModelAccessInformation; } /** @@ -18386,6 +18393,853 @@ declare module 'vscode' { */ readonly additionalCommonProperties?: Record; } + + /** + * Represents a user request in chat history. + */ + export class ChatRequestTurn { + /** + * The prompt as entered by the user. + * + * Information about references used in this request is stored in {@link ChatRequestTurn.references}. + * + * *Note* that the {@link ChatParticipant.name name} of the participant and the {@link ChatCommand.name command} + * are not part of the prompt. + */ + readonly prompt: string; + + /** + * The id of the chat participant to which this request was directed. + */ + readonly participant: string; + + /** + * The name of the {@link ChatCommand command} that was selected for this request. + */ + readonly command?: string; + + /** + * The references that were used in this message. + */ + readonly references: ChatPromptReference[]; + + /** + * @hidden + */ + private constructor(prompt: string, command: string | undefined, references: ChatPromptReference[], participant: string); + } + + /** + * Represents a chat participant's response in chat history. + */ + export class ChatResponseTurn { + /** + * The content that was received from the chat participant. Only the stream parts that represent actual content (not metadata) are represented. + */ + readonly response: ReadonlyArray; + + /** + * The result that was received from the chat participant. + */ + readonly result: ChatResult; + + /** + * The id of the chat participant that this response came from. + */ + readonly participant: string; + + /** + * The name of the command that this response came from. + */ + readonly command?: string; + + /** + * @hidden + */ + private constructor(response: ReadonlyArray, result: ChatResult, participant: string); + } + + /** + * Extra context passed to a participant. + */ + export interface ChatContext { + /** + * All of the chat messages so far in the current chat session. Currently, only chat messages for the current participant are included. + */ + readonly history: ReadonlyArray; + } + + /** + * Represents an error result from a chat request. + */ + export interface ChatErrorDetails { + /** + * An error message that is shown to the user. + */ + message: string; + + /** + * If set to true, the response will be partly blurred out. + */ + responseIsFiltered?: boolean; + } + + /** + * The result of a chat request. + */ + export interface ChatResult { + /** + * If the request resulted in an error, this property defines the error details. + */ + errorDetails?: ChatErrorDetails; + + /** + * Arbitrary metadata for this result. Can be anything, but must be JSON-stringifyable. + */ + readonly metadata?: { readonly [key: string]: any }; + } + + /** + * Represents the type of user feedback received. + */ + export enum ChatResultFeedbackKind { + /** + * The user marked the result as helpful. + */ + Unhelpful = 0, + + /** + * The user marked the result as unhelpful. + */ + Helpful = 1, + } + + /** + * Represents user feedback for a result. + */ + export interface ChatResultFeedback { + /** + * The ChatResult for which the user is providing feedback. + * This object has the same properties as the result returned from the participant callback, including `metadata`, but is not the same instance. + */ + readonly result: ChatResult; + + /** + * The kind of feedback that was received. + */ + readonly kind: ChatResultFeedbackKind; + } + + /** + * A followup question suggested by the participant. + */ + export interface ChatFollowup { + /** + * The message to send to the chat. + */ + prompt: string; + + /** + * A title to show the user. The prompt will be shown by default, when this is unspecified. + */ + label?: string; + + /** + * 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; + + /** + * By default, the followup goes to the same participant/command. But this property can be set to invoke a different command. + */ + command?: string; + } + + /** + * Will be invoked once after each request to get suggested followup questions to show the user. The user can click the followup to send it to the chat. + */ + export interface ChatFollowupProvider { + /** + * Provide followups for the given result. + * @param result This object has the same properties as the result returned from the participant callback, including `metadata`, but is not the same instance. + * @param token A cancellation token. + */ + provideFollowups(result: ChatResult, context: ChatContext, token: CancellationToken): ProviderResult; + } + + /** + * A chat request handler is a callback that will be invoked when a request is made to a chat participant. + */ + export type ChatRequestHandler = (request: ChatRequest, context: ChatContext, response: ChatResponseStream, token: CancellationToken) => ProviderResult; + + /** + * A chat participant can be invoked by the user in a chat session, using the `@` prefix. When it is invoked, it handles the chat request and is solely + * responsible for providing a response to the user. A ChatParticipant is created using {@link chat.createChatParticipant}. + */ + export interface ChatParticipant { + /** + * A unique ID for this participant. + */ + readonly id: string; + + /** + * An icon for the participant shown in UI. + */ + iconPath?: Uri | { + /** + * The icon path for the light theme. + */ + light: Uri; + /** + * The icon path for the dark theme. + */ + dark: Uri; + } | ThemeIcon; + + /** + * The handler for requests to this participant. + */ + requestHandler: ChatRequestHandler; + + /** + * This provider will be called once after each request to retrieve suggested followup questions. + */ + followupProvider?: ChatFollowupProvider; + + /** + * An event that fires whenever feedback for a result is received, e.g. when a user up- or down-votes + * a result. + * + * The passed {@link ChatResultFeedback.result result} is guaranteed to be the same instance that was + * previously returned from this chat participant. + */ + onDidReceiveFeedback: Event; + + /** + * Dispose this participant and free resources. + */ + dispose(): void; + } + + /** + * A reference to a value that the user added to their chat request. + */ + export interface ChatPromptReference { + /** + * A unique identifier for this kind of reference. + */ + readonly id: string; + + /** + * The start and end index of the reference in the {@link ChatRequest.prompt prompt}. When undefined, the reference was not part of the prompt text. + * + * *Note* that the indices take the leading `#`-character into account which means they can + * used to modify the prompt as-is. + */ + readonly range?: [start: number, end: number]; + + /** + * A description of this value that could be used in an LLM prompt. + */ + readonly modelDescription?: string; + + /** + * The value of this reference. The `string | Uri | Location` types are used today, but this could expand in the future. + */ + readonly value: string | Uri | Location | unknown; + } + + /** + * A request to a chat participant. + */ + export interface ChatRequest { + /** + * The prompt as entered by the user. + * + * Information about references used in this request is stored in {@link ChatRequest.references}. + * + * *Note* that the {@link ChatParticipant.name name} of the participant and the {@link ChatCommand.name command} + * are not part of the prompt. + */ + readonly prompt: string; + + /** + * The name of the {@link ChatCommand command} that was selected for this request. + */ + readonly command: string | undefined; + + /** + * The list of references and their values that are referenced in the prompt. + * + * *Note* that the prompt contains references as authored and that it is up to the participant + * to further modify the prompt, for instance by inlining reference values or creating links to + * headings which contain the resolved values. References are sorted in reverse by their range + * in the prompt. That means the last reference in the prompt is the first in this list. This simplifies + * string-manipulation of the prompt. + */ + readonly references: readonly ChatPromptReference[]; + } + + /** + * The ChatResponseStream is how a participant is able to return content to the chat view. It provides several methods for streaming different types of content + * which will be rendered in an appropriate way in the chat view. A participant can use the helper method for the type of content it wants to return, or it + * can instantiate a {@link ChatResponsePart} and use the generic {@link ChatResponseStream.push} method to return it. + */ + export interface ChatResponseStream { + /** + * Push a markdown part to this stream. Short-hand for + * `push(new ChatResponseMarkdownPart(value))`. + * + * @see {@link ChatResponseStream.push} + * @param value A markdown string or a string that should be interpreted as markdown. The boolean form of {@link MarkdownString.isTrusted} is NOT supported. + */ + markdown(value: string | MarkdownString): void; + + /** + * Push an anchor part to this stream. Short-hand for + * `push(new ChatResponseAnchorPart(value, title))`. + * An anchor is an inline reference to some type of resource. + * + * @param value A uri, location, or symbol information. + * @param title An optional title that is rendered with value. + */ + anchor(value: Uri | Location, title?: string): void; + + /** + * Push a command button part to this stream. Short-hand for + * `push(new ChatResponseCommandButtonPart(value, title))`. + * + * @param command A Command that will be executed when the button is clicked. + */ + button(command: Command): void; + + /** + * Push a filetree part to this stream. Short-hand for + * `push(new ChatResponseFileTreePart(value))`. + * + * @param value File tree data. + * @param baseUri The base uri to which this file tree is relative. + */ + filetree(value: ChatResponseFileTree[], baseUri: Uri): void; + + /** + * Push a progress part to this stream. Short-hand for + * `push(new ChatResponseProgressPart(value))`. + * + * @param value A progress message + */ + progress(value: string): void; + + /** + * Push a reference to this stream. Short-hand for + * `push(new ChatResponseReferencePart(value))`. + * + * *Note* that the reference is not rendered inline with the response. + * + * @param value A uri or location + * @param iconPath Icon for the reference shown in UI + */ + reference(value: Uri | Location, iconPath?: Uri | ThemeIcon | { + /** + * The icon path for the light theme. + */ + light: Uri; + /** + * The icon path for the dark theme. + */ + dark: Uri; + }): void; + + /** + * Pushes a part to this stream. + * + * @param part A response part, rendered or metadata + */ + push(part: ChatResponsePart): void; + } + + /** + * Represents a part of a chat response that is formatted as Markdown. + */ + export class ChatResponseMarkdownPart { + /** + * A markdown string or a string that should be interpreted as markdown. + */ + value: MarkdownString; + + /** + * Create a new ChatResponseMarkdownPart. + * + * @param value A markdown string or a string that should be interpreted as markdown. The boolean form of {@link MarkdownString.isTrusted} is NOT supported. + */ + constructor(value: string | MarkdownString); + } + + /** + * Represents a file tree structure in a chat response. + */ + export interface ChatResponseFileTree { + /** + * The name of the file or directory. + */ + name: string; + + /** + * An array of child file trees, if the current file tree is a directory. + */ + children?: ChatResponseFileTree[]; + } + + /** + * Represents a part of a chat response that is a file tree. + */ + export class ChatResponseFileTreePart { + /** + * File tree data. + */ + value: ChatResponseFileTree[]; + + /** + * The base uri to which this file tree is relative + */ + baseUri: Uri; + + /** + * Create a new ChatResponseFileTreePart. + * @param value File tree data. + * @param baseUri The base uri to which this file tree is relative. + */ + constructor(value: ChatResponseFileTree[], baseUri: Uri); + } + + /** + * Represents a part of a chat response that is an anchor, that is rendered as a link to a target. + */ + export class ChatResponseAnchorPart { + /** + * The target of this anchor. + */ + value: Uri | Location; + + /** + * An optional title that is rendered with value. + */ + title?: string; + + /** + * Create a new ChatResponseAnchorPart. + * @param value A uri or location. + * @param title An optional title that is rendered with value. + */ + constructor(value: Uri | Location, title?: string); + } + + /** + * Represents a part of a chat response that is a progress message. + */ + export class ChatResponseProgressPart { + /** + * The progress message + */ + value: string; + + /** + * Create a new ChatResponseProgressPart. + * @param value A progress message + */ + constructor(value: string); + } + + /** + * Represents a part of a chat response that is a reference, rendered separately from the content. + */ + export class ChatResponseReferencePart { + /** + * The reference target. + */ + value: Uri | Location; + + /** + * The icon for the reference. + */ + iconPath?: Uri | ThemeIcon | { + /** + * The icon path for the light theme. + */ + light: Uri; + /** + * The icon path for the dark theme. + */ + dark: Uri; + }; + + /** + * Create a new ChatResponseReferencePart. + * @param value A uri or location + * @param iconPath Icon for the reference shown in UI + */ + constructor(value: Uri | Location, iconPath?: Uri | ThemeIcon | { + /** + * The icon path for the light theme. + */ + light: Uri; + /** + * The icon path for the dark theme. + */ + dark: Uri; + }); + } + + /** + * Represents a part of a chat response that is a button that executes a command. + */ + export class ChatResponseCommandButtonPart { + /** + * The command that will be executed when the button is clicked. + */ + value: Command; + + /** + * Create a new ChatResponseCommandButtonPart. + * @param value A Command that will be executed when the button is clicked. + */ + constructor(value: Command); + } + + /** + * Represents the different chat response types. + */ + export type ChatResponsePart = ChatResponseMarkdownPart | ChatResponseFileTreePart | ChatResponseAnchorPart + | ChatResponseProgressPart | ChatResponseReferencePart | ChatResponseCommandButtonPart; + + + /** + * Namespace for chat functionality. Users interact with chat participants by sending messages + * to them in the chat view. Chat participants can respond with markdown or other types of content + * via the {@link ChatResponseStream}. + */ + export namespace chat { + /** + * Create a new {@link ChatParticipant chat participant} instance. + * + * @param id A unique identifier for the participant. + * @param handler A request handler for the participant. + * @returns A new chat participant + */ + export function createChatParticipant(id: string, handler: ChatRequestHandler): ChatParticipant; + } + + /** + * Represents the role of a chat message. This is either the user or the assistant. + */ + export enum LanguageModelChatMessageRole { + /** + * The user role, e.g the human interacting with a language model. + */ + User = 1, + + /** + * The assistant role, e.g. the language model generating responses. + */ + Assistant = 2 + } + + /** + * Represents a message in a chat. Can assume different roles, like user or assistant. + */ + export class LanguageModelChatMessage { + + /** + * Utility to create a new user message. + * + * @param content The content of the message. + * @param name The optional name of a user for the message. + */ + static User(content: string, name?: string): LanguageModelChatMessage; + + /** + * Utility to create a new assistant message. + * + * @param content The content of the message. + * @param name The optional name of a user for the message. + */ + static Assistant(content: string, name?: string): LanguageModelChatMessage; + + /** + * The role of this message. + */ + role: LanguageModelChatMessageRole; + + /** + * The content of this message. + */ + content: string; + + /** + * The optional name of a user for this message. + */ + name: string | undefined; + + /** + * Create a new user message. + * + * @param role The role of the message. + * @param content The content of the message. + * @param name The optional name of a user for the message. + */ + constructor(role: LanguageModelChatMessageRole, content: string, name?: string); + } + + /** + * Represents a language model response. + * + * @see {@link LanguageModelAccess.chatRequest} + */ + export interface LanguageModelChatResponse { + + /** + * An async iterable that is a stream of text chunks forming the overall response. + * + * *Note* that this stream will error when during data receiving an error occurs. Consumers of + * the stream should handle the errors accordingly. + * + * @example + * ```ts + * try { + * // consume stream + * for await (const chunk of response.stream) { + * console.log(chunk); + * } + * + * } catch(e) { + * // stream ended with an error + * console.error(e); + * } + * ``` + * + * To cancel the stream, the consumer can {@link CancellationTokenSource.cancel cancel} the token that was used to make the request + * or break from the for-loop. + */ + text: AsyncIterable; + } + + /** + * Represents a language model for making chat requests. + * + * @see {@link lm.selectChatModels} + */ + export interface LanguageModelChat { + + /** + * Human-readable name of the language model. + */ + readonly name: string; + + /** + * Opaque identifier of the language model. + */ + readonly id: string; + + /** + * A well-know identifier of the vendor of the language model, a sample is `copilot`, but + * values are defined by extensions contributing chat models and need to be looked up with them. + */ + readonly vendor: string; + + /** + * Opaque family-name of the language model. Values might be `gpt-3.5-turbo`, `gpt4`, `phi2`, or `llama` + * but they are defined by extensions contributing languages and subject to change. + */ + readonly family: string; + + /** + * Opaque version string of the model. This is defined by the extension contributing the language model + * and subject to change. + */ + readonly version: string; + + /** + * The maximum number of tokens that can be sent to the model in a single request. + */ + readonly maxInputTokens: number; + + /** + * Make a chat request using a language model. + * + * *Note* that language model use may be subject to access restrictions and user consent. Calling this function + * for the first time (for a extension) will show a consent dialog to the user and because of that this function + * must _only be called in response to a user action!_ Extension can use {@link LanguageModelAccessInformation.canSendRequest} + * to check if they have the necessary permissions to make a request. + * + * This function will return a rejected promise if making a request to the language model is not + * possible. Reasons for this can be: + * + * - user consent not given, see {@link LanguageModelError.NoPermissions `NoPermissions`} + * - model does not exist anymore, see {@link LanguageModelError.NotFound `NotFound`} + * - quota limits exceeded, see {@link LanguageModelError.Blocked `Blocked`} + * - other issues in which case extension must check {@link LanguageModelError.cause `LanguageModelError.cause`} + * + * @param messages An array of message instances. + * @param options Options that control the request. + * @param token A cancellation token which controls the request. See {@link CancellationTokenSource} for how to create one. + * @returns A thenable that resolves to a {@link LanguageModelChatResponse}. The promise will reject when the request couldn't be made. + */ + sendRequest(messages: LanguageModelChatMessage[], options?: LanguageModelChatRequestOptions, token?: CancellationToken): Thenable; + + /** + * Count the number of tokens in a message using the model specific tokenizer-logic. + + * @param text A string or a message instance. + * @param token Optional cancellation token. See {@link CancellationTokenSource} for how to create one. + * @returns A thenable that resolves to the number of tokens. + */ + countTokens(text: string | LanguageModelChatMessage, token?: CancellationToken): Thenable; + } + + /** + * Describes how to select language models for chat requests. + * + * @see {@link lm.selectChatModels} + */ + export interface LanguageModelChatSelector { + + /** + * A vendor of language models. + * @see {@link LanguageModelChat.vendor} + */ + vendor?: string; + + /** + * A family of language models. + * @see {@link LanguageModelChat.family} + */ + family?: string; + + /** + * The version of a language model. + * @see {@link LanguageModelChat.version} + */ + version?: string; + + /** + * The identifier of a language model. + * @see {@link LanguageModelChat.id} + */ + id?: string; + } + + /** + * An error type for language model specific errors. + * + * Consumers of language models should check the code property to determine specific + * failure causes, like `if(someError.code === vscode.LanguageModelError.NotFound.name) {...}` + * for the case of referring to an unknown language model. For unspecified errors the `cause`-property + * will contain the actual error. + */ + export class LanguageModelError extends Error { + + /** + * The requestor does not have permissions to use this + * language model + */ + static NoPermissions(message?: string): LanguageModelError; + + /** + * The requestor is blocked from using this language model. + */ + static Blocked(message?: string): LanguageModelError; + + /** + * The language model does not exist. + */ + static NotFound(message?: string): LanguageModelError; + + /** + * A code that identifies this error. + * + * Possible values are names of errors, like {@linkcode LanguageModelError.NotFound NotFound}, + * or `Unknown` for unspecified errors from the language model itself. In the latter case the + * `cause`-property will contain the actual error. + */ + readonly code: string; + } + + /** + * Options for making a chat request using a language model. + * + * @see {@link LanguageModelChat.sendRequest} + */ + export interface LanguageModelChatRequestOptions { + + /** + * A human-readable message that explains why access to a language model is needed and what feature is enabled by it. + */ + justification?: string; + + /** + * A set of options that control the behavior of the language model. These options are specific to the language model + * and need to be lookup in the respective documentation. + */ + modelOptions?: { [name: string]: any }; + } + + /** + * Namespace for language model related functionality. + */ + export namespace lm { + + /** + * An event that is fired when the set of available chat models changes. + */ + export const onDidChangeChatModels: Event; + + /** + * Select chat models by a {@link LanguageModelChatSelector selector}. This can yield in multiple or no chat models and + * extensions must handle these cases, esp. when no chat model exists, gracefully. + * + * ```ts + * + * const models = await vscode.lm.selectChatModels({family: 'gpt-3.5-turbo'})!; + * if (models.length > 0) { + * const [first] = models; + * const response = await first.sendRequest(...) + * // ... + * } else { + * // NO chat models available + * } + * ``` + * + * *Note* that extensions can hold-on to the results returned by this function and use them later. However, when the + * {@link onDidChangeChatModels}-event is fired the list of chat models might have changed and extensions should re-query. + * + * @param selector A chat model selector. When omitted all chat models are returned. + * @returns An array of chat models, can be empty! + */ + export function selectChatModels(selector?: LanguageModelChatSelector): Thenable; + } + + /** + * Represents extension specific information about the access to language models. + */ + export interface LanguageModelAccessInformation { + + /** + * An event that fires when access information changes. + */ + onDidChange: Event; + + /** + * Checks if a request can be made to a language model. + * + * *Note* that calling this function will not trigger a consent UI but just checks for a persisted state. + * + * @param chat A language model chat object. + * @return `true` if a request can be made, `false` if not, `undefined` if the language + * model does not exist or consent hasn't been asked for. + */ + canSendRequest(chat: LanguageModelChat): boolean | undefined; + } } /** diff --git a/src/vscode-dts/vscode.proposed.chatParticipant.d.ts b/src/vscode-dts/vscode.proposed.chatParticipant.d.ts deleted file mode 100644 index 9ace1a09551..00000000000 --- a/src/vscode-dts/vscode.proposed.chatParticipant.d.ts +++ /dev/null @@ -1,505 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -declare module 'vscode' { - - /** - * Represents a user request in chat history. - */ - export class ChatRequestTurn { - /** - * The prompt as entered by the user. - * - * Information about references used in this request is stored in {@link ChatRequestTurn.references}. - * - * *Note* that the {@link ChatParticipant.name name} of the participant and the {@link ChatCommand.name command} - * are not part of the prompt. - */ - readonly prompt: string; - - /** - * The id of the chat participant to which this request was directed. - */ - readonly participant: string; - - /** - * The name of the {@link ChatCommand command} that was selected for this request. - */ - readonly command?: string; - - /** - * The references that were used in this message. - */ - readonly references: ChatPromptReference[]; - - /** - * @hidden - */ - private constructor(prompt: string, command: string | undefined, references: ChatPromptReference[], participant: string); - } - - /** - * Represents a chat participant's response in chat history. - */ - export class ChatResponseTurn { - /** - * The content that was received from the chat participant. Only the stream parts that represent actual content (not metadata) are represented. - */ - readonly response: ReadonlyArray; - - /** - * The result that was received from the chat participant. - */ - readonly result: ChatResult; - - /** - * The id of the chat participant that this response came from. - */ - readonly participant: string; - - /** - * The name of the command that this response came from. - */ - readonly command?: string; - - /** - * @hidden - */ - private constructor(response: ReadonlyArray, result: ChatResult, participant: string); - } - - /** - * Extra context passed to a participant. - */ - export interface ChatContext { - /** - * All of the chat messages so far in the current chat session. Currently, only chat messages for the current participant are included. - */ - readonly history: ReadonlyArray; - } - - /** - * Represents an error result from a chat request. - */ - export interface ChatErrorDetails { - /** - * An error message that is shown to the user. - */ - message: string; - - /** - * If set to true, the response will be partly blurred out. - */ - responseIsFiltered?: boolean; - } - - /** - * The result of a chat request. - */ - export interface ChatResult { - /** - * If the request resulted in an error, this property defines the error details. - */ - errorDetails?: ChatErrorDetails; - - /** - * Arbitrary metadata for this result. Can be anything, but must be JSON-stringifyable. - */ - readonly metadata?: { readonly [key: string]: any }; - } - - /** - * Represents the type of user feedback received. - */ - export enum ChatResultFeedbackKind { - /** - * The user marked the result as helpful. - */ - Unhelpful = 0, - - /** - * The user marked the result as unhelpful. - */ - Helpful = 1, - } - - /** - * Represents user feedback for a result. - */ - export interface ChatResultFeedback { - /** - * The ChatResult for which the user is providing feedback. - * This object has the same properties as the result returned from the participant callback, including `metadata`, but is not the same instance. - */ - readonly result: ChatResult; - - /** - * The kind of feedback that was received. - */ - readonly kind: ChatResultFeedbackKind; - } - - /** - * A followup question suggested by the participant. - */ - export interface ChatFollowup { - /** - * The message to send to the chat. - */ - prompt: string; - - /** - * A title to show the user. The prompt will be shown by default, when this is unspecified. - */ - label?: string; - - /** - * 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; - - /** - * By default, the followup goes to the same participant/command. But this property can be set to invoke a different command. - */ - command?: string; - } - - /** - * Will be invoked once after each request to get suggested followup questions to show the user. The user can click the followup to send it to the chat. - */ - export interface ChatFollowupProvider { - /** - * Provide followups for the given result. - * @param result This object has the same properties as the result returned from the participant callback, including `metadata`, but is not the same instance. - * @param token A cancellation token. - */ - provideFollowups(result: ChatResult, context: ChatContext, token: CancellationToken): ProviderResult; - } - - /** - * A chat request handler is a callback that will be invoked when a request is made to a chat participant. - */ - export type ChatRequestHandler = (request: ChatRequest, context: ChatContext, response: ChatResponseStream, token: CancellationToken) => ProviderResult; - - /** - * A chat participant can be invoked by the user in a chat session, using the `@` prefix. When it is invoked, it handles the chat request and is solely - * responsible for providing a response to the user. A ChatParticipant is created using {@link chat.createChatParticipant}. - */ - export interface ChatParticipant { - /** - * A unique ID for this participant. - */ - readonly id: string; - - /** - * An icon for the participant shown in UI. - */ - iconPath?: Uri | { - /** - * The icon path for the light theme. - */ - light: Uri; - /** - * The icon path for the dark theme. - */ - dark: Uri; - } | ThemeIcon; - - /** - * The handler for requests to this participant. - */ - requestHandler: ChatRequestHandler; - - /** - * This provider will be called once after each request to retrieve suggested followup questions. - */ - followupProvider?: ChatFollowupProvider; - - /** - * An event that fires whenever feedback for a result is received, e.g. when a user up- or down-votes - * a result. - * - * The passed {@link ChatResultFeedback.result result} is guaranteed to be the same instance that was - * previously returned from this chat participant. - */ - onDidReceiveFeedback: Event; - - /** - * Dispose this participant and free resources. - */ - dispose(): void; - } - - export interface ChatPromptReference { - /** - * A unique identifier for this kind of reference. - */ - readonly id: string; - - /** - * The start and end index of the reference in the {@link ChatRequest.prompt prompt}. When undefined, the reference was not part of the prompt text. - * - * *Note* that the indices take the leading `#`-character into account which means they can - * used to modify the prompt as-is. - */ - readonly range?: [start: number, end: number]; - - /** - * A description of this value that could be used in an LLM prompt. - */ - readonly modelDescription?: string; - - /** - * The value of this reference. The `string | Uri | Location` types are used today, but this could expand in the future. - */ - readonly value: string | Uri | Location | unknown; - } - - export interface ChatRequest { - /** - * The prompt as entered by the user. - * - * Information about references used in this request is stored in {@link ChatRequest.references}. - * - * *Note* that the {@link ChatParticipant.name name} of the participant and the {@link ChatCommand.name command} - * are not part of the prompt. - */ - readonly prompt: string; - - /** - * The name of the {@link ChatCommand command} that was selected for this request. - */ - readonly command: string | undefined; - - /** - * The list of references and their values that are referenced in the prompt. - * - * *Note* that the prompt contains references as authored and that it is up to the participant - * to further modify the prompt, for instance by inlining reference values or creating links to - * headings which contain the resolved values. References are sorted in reverse by their range - * in the prompt. That means the last reference in the prompt is the first in this list. This simplifies - * string-manipulation of the prompt. - */ - readonly references: readonly ChatPromptReference[]; - } - - /** - * The ChatResponseStream is how a participant is able to return content to the chat view. It provides several methods for streaming different types of content - * which will be rendered in an appropriate way in the chat view. A participant can use the helper method for the type of content it wants to return, or it - * can instantiate a {@link ChatResponsePart} and use the generic {@link ChatResponseStream.push} method to return it. - */ - export interface ChatResponseStream { - /** - * Push a markdown part to this stream. Short-hand for - * `push(new ChatResponseMarkdownPart(value))`. - * - * @see {@link ChatResponseStream.push} - * @param value A markdown string or a string that should be interpreted as markdown. The boolean form of {@link MarkdownString.isTrusted} is NOT supported. - */ - markdown(value: string | MarkdownString): void; - - /** - * Push an anchor part to this stream. Short-hand for - * `push(new ChatResponseAnchorPart(value, title))`. - * An anchor is an inline reference to some type of resource. - * - * @param value A uri, location, or symbol information. - * @param title An optional title that is rendered with value. - */ - anchor(value: Uri | Location, title?: string): void; - - /** - * Push a command button part to this stream. Short-hand for - * `push(new ChatResponseCommandButtonPart(value, title))`. - * - * @param command A Command that will be executed when the button is clicked. - */ - button(command: Command): void; - - /** - * Push a filetree part to this stream. Short-hand for - * `push(new ChatResponseFileTreePart(value))`. - * - * @param value File tree data. - * @param baseUri The base uri to which this file tree is relative. - */ - filetree(value: ChatResponseFileTree[], baseUri: Uri): void; - - /** - * Push a progress part to this stream. Short-hand for - * `push(new ChatResponseProgressPart(value))`. - * - * @param value A progress message - */ - progress(value: string): void; - - /** - * Push a reference to this stream. Short-hand for - * `push(new ChatResponseReferencePart(value))`. - * - * *Note* that the reference is not rendered inline with the response. - * - * @param value A uri or location - * @param iconPath Icon for the reference shown in UI - */ - reference(value: Uri | Location, iconPath?: Uri | ThemeIcon | { light: Uri; dark: Uri }): void; - - /** - * Pushes a part to this stream. - * - * @param part A response part, rendered or metadata - */ - push(part: ChatResponsePart): void; - } - - /** - * Represents a part of a chat response that is formatted as Markdown. - */ - export class ChatResponseMarkdownPart { - /** - * A markdown string or a string that should be interpreted as markdown. - */ - value: MarkdownString; - - /** - * Create a new ChatResponseMarkdownPart. - * - * @param value A markdown string or a string that should be interpreted as markdown. The boolean form of {@link MarkdownString.isTrusted} is NOT supported. - */ - constructor(value: string | MarkdownString); - } - - /** - * Represents a file tree structure in a chat response. - */ - export interface ChatResponseFileTree { - /** - * The name of the file or directory. - */ - name: string; - - /** - * An array of child file trees, if the current file tree is a directory. - */ - children?: ChatResponseFileTree[]; - } - - /** - * Represents a part of a chat response that is a file tree. - */ - export class ChatResponseFileTreePart { - /** - * File tree data. - */ - value: ChatResponseFileTree[]; - - /** - * The base uri to which this file tree is relative - */ - baseUri: Uri; - - /** - * Create a new ChatResponseFileTreePart. - * @param value File tree data. - * @param baseUri The base uri to which this file tree is relative. - */ - constructor(value: ChatResponseFileTree[], baseUri: Uri); - } - - /** - * Represents a part of a chat response that is an anchor, that is rendered as a link to a target. - */ - export class ChatResponseAnchorPart { - /** - * The target of this anchor. - */ - value: Uri | Location; - - /** - * An optional title that is rendered with value. - */ - title?: string; - - /** - * Create a new ChatResponseAnchorPart. - * @param value A uri or location. - * @param title An optional title that is rendered with value. - */ - constructor(value: Uri | Location, title?: string); - } - - /** - * Represents a part of a chat response that is a progress message. - */ - export class ChatResponseProgressPart { - /** - * The progress message - */ - value: string; - - /** - * Create a new ChatResponseProgressPart. - * @param value A progress message - */ - constructor(value: string); - } - - /** - * Represents a part of a chat response that is a reference, rendered separately from the content. - */ - export class ChatResponseReferencePart { - /** - * The reference target. - */ - value: Uri | Location; - - /** - * The icon for the reference. - */ - iconPath?: Uri | ThemeIcon | { light: Uri; dark: Uri }; - - /** - * Create a new ChatResponseReferencePart. - * @param value A uri or location - * @param iconPath Icon for the reference shown in UI - */ - constructor(value: Uri | Location, iconPath?: Uri | ThemeIcon | { light: Uri; dark: Uri }); - } - - /** - * Represents a part of a chat response that is a button that executes a command. - */ - export class ChatResponseCommandButtonPart { - /** - * The command that will be executed when the button is clicked. - */ - value: Command; - - /** - * Create a new ChatResponseCommandButtonPart. - * @param value A Command that will be executed when the button is clicked. - */ - constructor(value: Command); - } - - /** - * Represents the different chat response types. - */ - export type ChatResponsePart = ChatResponseMarkdownPart | ChatResponseFileTreePart | ChatResponseAnchorPart - | ChatResponseProgressPart | ChatResponseReferencePart | ChatResponseCommandButtonPart; - - - export namespace chat { - /** - * Create a new {@link ChatParticipant chat participant} instance. - * - * @param id A unique identifier for the participant. - * @param handler A request handler for the participant. - * @returns A new chat participant - */ - export function createChatParticipant(id: string, handler: ChatRequestHandler): ChatParticipant; - } -} diff --git a/src/vscode-dts/vscode.proposed.languageModels.d.ts b/src/vscode-dts/vscode.proposed.languageModels.d.ts deleted file mode 100644 index 2024eb9fda4..00000000000 --- a/src/vscode-dts/vscode.proposed.languageModels.d.ts +++ /dev/null @@ -1,330 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -declare module 'vscode' { - - // https://github.com/microsoft/vscode/issues/206265 - - /** - * Represents the role of a chat message. This is either the user or the assistant. - */ - export enum LanguageModelChatMessageRole { - /** - * The user role, e.g the human interacting with a language model. - */ - User = 1, - - /** - * The assistant role, e.g. the language model generating responses. - */ - Assistant = 2 - } - - /** - * Represents a message in a chat. Can assume different roles, like user or assistant. - */ - export class LanguageModelChatMessage { - - /** - * Utility to create a new user message. - * - * @param content The content of the message. - * @param name The optional name of a user for the message. - */ - static User(content: string, name?: string): LanguageModelChatMessage; - - /** - * Utility to create a new assistant message. - * - * @param content The content of the message. - * @param name The optional name of a user for the message. - */ - static Assistant(content: string, name?: string): LanguageModelChatMessage; - - /** - * The role of this message. - */ - role: LanguageModelChatMessageRole; - - /** - * The content of this message. - */ - content: string; - - /** - * The optional name of a user for this message. - */ - name: string | undefined; - - /** - * Create a new user message. - * - * @param role The role of the message. - * @param content The content of the message. - * @param name The optional name of a user for the message. - */ - constructor(role: LanguageModelChatMessageRole, content: string, name?: string); - } - - /** - * Represents a language model response. - * - * @see {@link LanguageModelAccess.chatRequest} - */ - export interface LanguageModelChatResponse { - - /** - * An async iterable that is a stream of text chunks forming the overall response. - * - * *Note* that this stream will error when during data receiving an error occurs. Consumers of - * the stream should handle the errors accordingly. - * - * @example - * ```ts - * try { - * // consume stream - * for await (const chunk of response.stream) { - * console.log(chunk); - * } - * - * } catch(e) { - * // stream ended with an error - * console.error(e); - * } - * ``` - * - * To cancel the stream, the consumer can {@link CancellationTokenSource.cancel cancel} the token that was used to make the request - * or break from the for-loop. - */ - text: AsyncIterable; - } - - /** - * Represents a language model for making chat requests. - * - * @see {@link lm.selectChatModels} - */ - export interface LanguageModelChat { - - /** - * Human-readable name of the language model. - */ - readonly name: string; - - /** - * Opaque identifier of the language model. - */ - readonly id: string; - - /** - * A well-know identifier of the vendor of the language model, a sample is `copilot`, but - * values are defined by extensions contributing chat models and need to be looked up with them. - */ - readonly vendor: string; - - /** - * Opaque family-name of the language model. Values might be `gpt-3.5-turbo`, `gpt4`, `phi2`, or `llama` - * but they are defined by extensions contributing languages and subject to change. - */ - readonly family: string; - - /** - * Opaque version string of the model. This is defined by the extension contributing the language model - * and subject to change. - */ - readonly version: string; - - /** - * The maximum number of tokens that can be sent to the model in a single request. - */ - readonly maxInputTokens: number; - - /** - * Make a chat request using a language model. - * - * *Note* that language model use may be subject to access restrictions and user consent. Calling this function - * for the first time (for a extension) will show a consent dialog to the user and because of that this function - * must _only be called in response to a user action!_ Extension can use {@link LanguageModelAccessInformation.canSendRequest} - * to check if they have the necessary permissions to make a request. - * - * This function will return a rejected promise if making a request to the language model is not - * possible. Reasons for this can be: - * - * - user consent not given, see {@link LanguageModelError.NoPermissions `NoPermissions`} - * - model does not exist anymore, see {@link LanguageModelError.NotFound `NotFound`} - * - quota limits exceeded, see {@link LanguageModelError.Blocked `Blocked`} - * - other issues in which case extension must check {@link LanguageModelError.cause `LanguageModelError.cause`} - * - * @param messages An array of message instances. - * @param options Options that control the request. - * @param token A cancellation token which controls the request. See {@link CancellationTokenSource} for how to create one. - * @returns A thenable that resolves to a {@link LanguageModelChatResponse}. The promise will reject when the request couldn't be made. - */ - sendRequest(messages: LanguageModelChatMessage[], options?: LanguageModelChatRequestOptions, token?: CancellationToken): Thenable; - - /** - * Count the number of tokens in a message using the model specific tokenizer-logic. - - * @param text A string or a message instance. - * @param token Optional cancellation token. See {@link CancellationTokenSource} for how to create one. - * @returns A thenable that resolves to the number of tokens. - */ - countTokens(text: string | LanguageModelChatMessage, token?: CancellationToken): Thenable; - } - - /** - * Describes how to select language models for chat requests. - * - * @see {@link lm.selectChatModels} - */ - export interface LanguageModelChatSelector { - - /** - * A vendor of language models. - * @see {@link LanguageModelChat.vendor} - */ - vendor?: string; - - /** - * A family of language models. - * @see {@link LanguageModelChat.family} - */ - family?: string; - - /** - * The version of a language model. - * @see {@link LanguageModelChat.version} - */ - version?: string; - - /** - * The identifier of a language model. - * @see {@link LanguageModelChat.id} - */ - id?: string; - } - - /** - * An error type for language model specific errors. - * - * Consumers of language models should check the code property to determine specific - * failure causes, like `if(someError.code === vscode.LanguageModelError.NotFound.name) {...}` - * for the case of referring to an unknown language model. For unspecified errors the `cause`-property - * will contain the actual error. - */ - export class LanguageModelError extends Error { - - /** - * The requestor does not have permissions to use this - * language model - */ - static NoPermissions(message?: string): LanguageModelError; - - /** - * The requestor is blocked from using this language model. - */ - static Blocked(message?: string): LanguageModelError; - - /** - * The language model does not exist. - */ - static NotFound(message?: string): LanguageModelError; - - /** - * A code that identifies this error. - * - * Possible values are names of errors, like {@linkcode LanguageModelError.NotFound NotFound}, - * or `Unknown` for unspecified errors from the language model itself. In the latter case the - * `cause`-property will contain the actual error. - */ - readonly code: string; - } - - /** - * Options for making a chat request using a language model. - * - * @see {@link LanguageModelChat.sendRequest} - */ - export interface LanguageModelChatRequestOptions { - - /** - * A human-readable message that explains why access to a language model is needed and what feature is enabled by it. - */ - justification?: string; - - /** - * A set of options that control the behavior of the language model. These options are specific to the language model - * and need to be lookup in the respective documentation. - */ - modelOptions?: { [name: string]: any }; - } - - /** - * Namespace for language model related functionality. - */ - export namespace lm { - - /** - * An event that is fired when the set of available chat models changes. - */ - export const onDidChangeChatModels: Event; - - /** - * Select chat models by a {@link LanguageModelChatSelector selector}. This can yield in multiple or no chat models and - * extensions must handle these cases, esp. when no chat model exists, gracefully. - * - * ```ts - * - * const models = await vscode.lm.selectChatModels({family: 'gpt-3.5-turbo'})!; - * if (models.length > 0) { - * const [first] = models; - * const response = await first.sendRequest(...) - * // ... - * } else { - * // NO chat models available - * } - * ``` - * - * *Note* that extensions can hold-on to the results returned by this function and use them later. However, when the - * {@link onDidChangeChatModels}-event is fired the list of chat models might have changed and extensions should re-query. - * - * @param selector A chat model selector. When omitted all chat models are returned. - * @returns An array of chat models, can be empty! - */ - export function selectChatModels(selector?: LanguageModelChatSelector): Thenable; - } - - /** - * Represents extension specific information about the access to language models. - */ - export interface LanguageModelAccessInformation { - - /** - * An event that fires when access information changes. - */ - onDidChange: Event; - - /** - * Checks if a request can be made to a language model. - * - * *Note* that calling this function will not trigger a consent UI but just checks for a persisted state. - * - * @param chat A language model chat object. - * @return `true` if a request can be made, `false` if not, `undefined` if the language - * model does not exist or consent hasn't been asked for. - */ - canSendRequest(chat: LanguageModelChat): boolean | undefined; - } - - export interface ExtensionContext { - - /** - * An object that keeps information about how this extension can use language models. - * - * @see {@link lm.sendChatRequest} - */ - readonly languageModelAccessInformation: LanguageModelAccessInformation; - } -} From 90402fb5df9e08eb86fdf5032296f1b5797a3d86 Mon Sep 17 00:00:00 2001 From: David Dossett Date: Wed, 15 May 2024 14:25:13 -0700 Subject: [PATCH 210/357] Remove chat attachments border and tweak styles (#212745) * Use chat-requestBorder and update attachments layout * Remove border and fix layout janks --- .../workbench/contrib/chat/browser/media/chat.css | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 8b09ad3c402..73bd547e584 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -489,22 +489,18 @@ } .interactive-session .chat-attached-context { - padding: 8px 8px 13px; - margin-right: -3px; - margin-top: 4px; - margin-bottom: -4px; - border: 1px solid var(--vscode-input-border, var(--vscode-input-background, transparent)); - border-radius: 6px 6px 0px 0px; + padding: 8px 0; + border-radius: 6px; display: flex; gap: 4px; flex-wrap: wrap; } .interactive-session .chat-attached-context .chat-attached-context-attachment { - padding: 3px; - border: 1px solid var(--vscode-input-border, var(--vscode-input-background, transparent)); + padding: 2px; + border: 1px solid var(--vscode-chat-requestBorder, var(--vscode-input-background, transparent)); border-radius: 5px; - height: 20px; + height: 18px; } .interactive-session-followups { From d1cc483e03313d7d84d18dcae1264a02f54bf9be Mon Sep 17 00:00:00 2001 From: David Dossett Date: Wed, 15 May 2024 14:37:04 -0700 Subject: [PATCH 211/357] Initial inline chat polish (#212755) * Initial inline chat polish * Fix widget padding * More tweaks --- .../contrib/chat/browser/media/chat.css | 4 +-- .../browser/inlineChatStrategies.ts | 6 ++-- .../inlineChat/browser/media/inlineChat.css | 30 ++++++++++++------- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 73bd547e584..a81d71bfbd1 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -343,7 +343,7 @@ cursor: text; background-color: var(--vscode-input-background); border: 1px solid var(--vscode-input-border, transparent); - border-radius: 4px; + border-radius: 2px; position: relative; padding: 0 6px; margin-bottom: 4px; @@ -471,7 +471,7 @@ .interactive-session .interactive-input-part.compact { margin: 0; - padding: 6px 0px; + padding: 8px 0 0 0 } .interactive-session .chat-attached-context .chat-attached-context-attachment { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts index 84d6a8db13e..6f7727d6f17 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts @@ -590,17 +590,17 @@ export class LiveStrategy extends EditModeStrategy { message = localize('change.0', "Nothing changed."); } else if (remaining === 1) { message = needsReview - ? localize('review.1', "$(info) Accept or Discard 1 change.") + ? localize('review.1', "$(info) Accept or discard 1 change") : localize('change.1', "1 change"); } else { message = needsReview - ? localize('review.N', "$(info) Accept or Discard {0} changes.", remaining) + ? localize('review.N', "$(info) Accept or Discard {0} changes", remaining) : localize('change.N', "{0} changes", total); } let title: string | undefined; if (needsReview) { - title = localize('review', "Review (accept or discard) all changes before continuing."); + title = localize('review', "Review (accept or discard) all changes before continuing"); } this._zone.widget.updateStatus(message, { title }); diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css index 28812fb55d1..e7b6f8a6c0b 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css @@ -17,11 +17,11 @@ .monaco-workbench .inline-chat { color: inherit; - padding: 6px; - border-radius: 6px; + padding: 0 8px 8px 8px; + border-radius: 4px; border: 1px solid var(--vscode-inlineChat-border); + box-shadow: 0 2px 4px 0 var(--vscode-widget-shadow); margin-top: 8px; - box-shadow: 0 -1px 6px var(--vscode-inlineChat-shadow); background: var(--vscode-inlineChat-background); } @@ -31,10 +31,24 @@ .monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-input-part .interactive-input-and-execute-toolbar { width: 100%; + border-radius: 2px; +} + +.monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-list { + padding: 4px 0 0 0; } .monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-list .interactive-item-container.interactive-item-compact { - padding: 4px + padding: 6px 4px; + gap: 6px; +} + +.monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-list .interactive-item-container.interactive-item-compact .header .avatar { + outline-offset: -1px; +} + +.monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-list .interactive-request { + border: none; } /* progress bit */ @@ -56,7 +70,6 @@ align-items: center; } - .monaco-workbench .inline-chat .status .actions.hidden { display: none; } @@ -64,9 +77,7 @@ .monaco-workbench .inline-chat .status .label { overflow: hidden; color: var(--vscode-descriptionForeground); - font-size: 11px; - padding-top: 4px; - padding-left: 10px; + font-size: 12px; display: inline-flex; } @@ -131,7 +142,6 @@ .monaco-workbench .inline-chat .status .actions { display: flex; - padding-top: 4px; } .monaco-workbench .inline-chat .status .actions > .monaco-button, @@ -239,7 +249,7 @@ .monaco-workbench .interactive-session .interactive-input-and-execute-toolbar .monaco-editor .inline-chat-slash-command { background-color: var(--vscode-chat-slashCommandBackground); color: var(--vscode-chat-slashCommandForeground); /* Overrides the foreground color rule in chat.css */ - border-radius: 4px; + border-radius: 2px; padding: 1px; } From 5c7e9ff4fdede79c6f3fed7fb3195c1305905cc9 Mon Sep 17 00:00:00 2001 From: David Dossett Date: Wed, 15 May 2024 14:41:42 -0700 Subject: [PATCH 212/357] Remove extra space above attachments --- src/vs/workbench/contrib/chat/browser/media/chat.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index a81d71bfbd1..ae05744e7b3 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -489,7 +489,7 @@ } .interactive-session .chat-attached-context { - padding: 8px 0; + padding: 0 0 8px 0; border-radius: 6px; display: flex; gap: 4px; From cee89583ba7fcdbeaf38e703024ffe0acdd89819 Mon Sep 17 00:00:00 2001 From: David Dossett Date: Wed, 15 May 2024 15:26:11 -0700 Subject: [PATCH 213/357] Use descriptionForeground for close icon --- src/vs/workbench/contrib/chat/browser/media/chat.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index ae05744e7b3..30ecbdd9517 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -488,6 +488,10 @@ align-items: center; } +.interactive-session .chat-attached-context .chat-attached-context-attachment .codicon { + color: var(--vscode-descriptionForeground); +} + .interactive-session .chat-attached-context { padding: 0 0 8px 0; border-radius: 6px; From 7c310e7b31e011bda853bc0808905d0320b344f8 Mon Sep 17 00:00:00 2001 From: David Dossett Date: Wed, 15 May 2024 15:40:48 -0700 Subject: [PATCH 214/357] Update codicons --- .../browser/ui/codicons/codicon/codicon.ttf | Bin 80348 -> 80340 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf index eda8771f7a8096e97507c267a99897ee44fd1183..27ee4c68caef1cd22342f481420d6dbda1648012 100644 GIT binary patch delta 1099 zcmV-R1ho6y^90oM1du@sZ76vF04JmX0QVG;Of&=qi^o=xgd-8S0005_aBp*TV;Dxl z00AJdk+?7fRQvgHvFt%3e;NZg1BwIW1P}yf1k42-1!@Iq1(*fu1_lPE2U-W32qXwX z2&xGj3DgRR3eF1l3kVBP3t|g&3x*4r3*Za-3=9ky3^oj643G@;4Jr*j4Q36F4g3x+ z4tx%_4$u!K4}cHI5CRY;5V8>75d;xp5u6e55+)K#61Wo<6JQh+e-t=zIhC>J;vNEcKWXcu-Dniv=uG8jx4ni&ciHW^46f*H&i{2CS-J{npY za2lc-#2V-u92+znv>W6c3LID*b{w1>yd2;i8XYzrSRIHRx*g^o2p%vVTpolTydM-F zmLJF;;vgCzcp$nVe;y%vA+jO_B5WeGBKjjbBcLO~Bk&{wBpxK9B)}xxB={vpC4?oo zCFUkrCdwy5Cypn$C^#ryDC8-4DfB9QD#j}cE1oOfEEFtCEQl>CEu<~TE@Cd^FCs5& zFMuz`FX}KBFjg>pFxoK+F>EoYG1f91GJG2`RG&FEDpfv6^rZw0$ zJ~pN{`Zqi`YBz#6nm4#N;5Y<0COAYmVmN>}m^ijL&N%Ej1UVczRymS6^>4cGCoQ^WIm8SyguST5=Abz#kYN znjg#`<{%y*d?3CdeW{xC5R=w zCF&+zCe9~DCz2<;C_E@)DCjACDflXYD#|MkE21mnEEp_KEQ~EKEvPNbE@m$1FD5T= zFN813FYYiJFj_ExFy1i^F>o=gG1@X9GJrC$GR`v`Gf*>he>37VI5c!Lq%`t1sx{m; zLN=;4{x?83Za0QEo;SQV1Xg-uaz&_?b7C&}BkU!=?96)G5 zl0eQu20=zae|ka6LHt50LS#aMLdHVwLv%x~L?}dZMOHN0LX* zNEAqDNZ?5{Nrp+BNz6(TN=iy%N{ULVOA1RqOUO(nOo&XnO$tpmO`J{4P4Z4kPG(MY zPcBcCPqLR5HdTsM=2jq9 zU{;1!x>od8K3AMq&R6VMC|I;v9$A=K{#ssI)LRT&Mq8j<6kJ+d#$8Tc6kal3P+vY@ za9@UBtY7wEJYZa4j$qPZVqt_~zG41iB4SQraAKxoJY#TUoMYl-nqShvV zOlHDnfBt7eXM|_KXa;CfXsT$=X&7m+Y6xryZN6>lZVqmGZlZ3^Zsu zaK3QPaNcn4aQ<;#ah`GBawu|ca^7V+jfo9}x{03RVM_0|o Date: Wed, 15 May 2024 15:50:03 -0700 Subject: [PATCH 215/357] Fix chat border radius --- src/vs/workbench/contrib/chat/browser/media/chat.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index ae05744e7b3..85cb529106f 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -343,7 +343,7 @@ cursor: text; background-color: var(--vscode-input-background); border: 1px solid var(--vscode-input-border, transparent); - border-radius: 2px; + border-radius: 4px; position: relative; padding: 0 6px; margin-bottom: 4px; @@ -353,6 +353,7 @@ .interactive-session .interactive-input-part.compact .interactive-input-and-execute-toolbar { margin-bottom: 0; + border-radius: 2px; } .interactive-session .interactive-input-and-side-toolbar { From 4b4eb7a000dd3e2e50adde71be3074672a2f926e Mon Sep 17 00:00:00 2001 From: David Dossett Date: Wed, 15 May 2024 17:13:46 -0700 Subject: [PATCH 216/357] Use latest references accordion styles --- .../contrib/chat/browser/chatListRenderer.ts | 2 +- src/vs/workbench/contrib/chat/browser/media/chat.css | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 0e3c34fec24..0875cb6bfd7 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -826,7 +826,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 869a8443b25..6effa1626ea 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -657,7 +657,7 @@ .interactive-session .chat-used-context { display: flex; flex-direction: column; - gap: 6px; + gap: 2px; } .interactive-response-progress-tree, @@ -690,13 +690,21 @@ .interactive-session .chat-used-context-label .monaco-button { /* unset Button styles */ display: inline-flex; + gap: 4px; width: 100%; border: none; - padding: 0; + border-radius: 4px; + padding: 4px 8px 4px 0; text-align: initial; justify-content: initial; } +.interactive-session .chat-used-context-label .monaco-button:hover { + background-color: var(--vscode-list-hoverBackground); + color: var(--vscode-foreground); + +} + .interactive-session .chat-used-context-label .monaco-text-button:focus { outline: none; } From 14ebfdc345b4f8866f6f693c8c2982883fa56d5a Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 15 May 2024 17:40:21 -0700 Subject: [PATCH 217/357] Add variable 'isSlow' (#212657) * Add variable 'isSlow' Since 'codebase' doesn't really work as a reference, I'm only making accessible to our agents for now * fix * Check for slow variables in parser as well --- .../src/singlefolder-tests/chat.test.ts | 2 +- src/vs/workbench/api/common/extHost.api.impl.ts | 4 ++-- src/vs/workbench/api/common/extHostChatAgents2.ts | 11 ++++++++++- src/vs/workbench/api/common/extHostChatVariables.ts | 4 ++-- .../chat/browser/contrib/chatInputCompletions.ts | 4 ++++ src/vs/workbench/contrib/chat/common/chatAgents.ts | 1 + .../contrib/chat/common/chatRequestParser.ts | 10 ++++++---- src/vs/workbench/contrib/chat/common/chatVariables.ts | 1 + .../vscode.proposed.chatParticipantAdditions.d.ts | 5 +++++ .../vscode.proposed.chatVariableResolver.d.ts | 6 ++++-- 10 files changed, 36 insertions(+), 12 deletions(-) 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 6ec0e74eea8..ca72f39feb8 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts @@ -71,7 +71,7 @@ suite('chat', () => { }); test('participant and variable', async () => { - disposables.push(chat.registerChatVariableResolver('myVarId', 'myVar', 'My variable', 'My variable', { + disposables.push(chat.registerChatVariableResolver('myVarId', 'myVar', 'My variable', 'My variable', false, { resolve(_name, _context, _token) { return [{ level: ChatVariableLevel.Full, value: 'myValue' }]; } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 64db6613bf3..cdf73ee943e 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1411,9 +1411,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'chatProvider'); return extHostLanguageModels.registerLanguageModel(extension, id, provider, metadata); }, - registerChatVariableResolver(id: string, name: string, userDescription: string, modelDescription: string | undefined, resolver: vscode.ChatVariableResolver) { + registerChatVariableResolver(id: string, name: string, userDescription: string, modelDescription: string | undefined, isSlow: boolean | undefined, resolver: vscode.ChatVariableResolver) { checkProposedApiEnabled(extension, 'chatVariableResolver'); - return extHostChatVariables.registerVariableResolver(extension, id, name, userDescription, modelDescription, resolver); + return extHostChatVariables.registerVariableResolver(extension, id, name, userDescription, modelDescription, isSlow, resolver); }, registerMappedEditsProvider(selector: vscode.DocumentSelector, provider: vscode.MappedEditsProvider) { checkProposedApiEnabled(extension, 'mappedEditsProvider'); diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 86518eac57a..eb8f742a2e5 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -476,6 +476,7 @@ class ExtHostChatAgent { private _agentVariableProvider?: { provider: vscode.ChatParticipantCompletionItemProvider; triggerCharacters: string[] }; private _welcomeMessageProvider?: vscode.ChatWelcomeMessageProvider | undefined; private _requester: vscode.ChatRequesterInformation | undefined; + private _supportsSlowReferences: boolean | undefined; constructor( public readonly extension: IExtensionDescription, @@ -573,7 +574,8 @@ class ExtHostChatAgent { helpTextVariablesPrefix: (!this._helpTextVariablesPrefix || typeof this._helpTextVariablesPrefix === 'string') ? this._helpTextVariablesPrefix : typeConvert.MarkdownString.from(this._helpTextVariablesPrefix), helpTextPostfix: (!this._helpTextPostfix || typeof this._helpTextPostfix === 'string') ? this._helpTextPostfix : typeConvert.MarkdownString.from(this._helpTextPostfix), supportIssueReporting: this._supportIssueReporting, - requester: this._requester + requester: this._requester, + supportsSlowVariables: this._supportsSlowReferences, }); updateScheduled = false; }); @@ -699,6 +701,13 @@ class ExtHostChatAgent { get requester() { return that._requester; }, + set supportsSlowReferences(v) { + that._supportsSlowReferences = v; + updateMetadataSoon(); + }, + get supportsSlowReferences() { + return that._supportsSlowReferences; + }, dispose() { disposed = true; that._followupProvider = undefined; diff --git a/src/vs/workbench/api/common/extHostChatVariables.ts b/src/vs/workbench/api/common/extHostChatVariables.ts index df3ecb47190..43a9dcceb53 100644 --- a/src/vs/workbench/api/common/extHostChatVariables.ts +++ b/src/vs/workbench/api/common/extHostChatVariables.ts @@ -52,10 +52,10 @@ export class ExtHostChatVariables implements ExtHostChatVariablesShape { return undefined; } - registerVariableResolver(extension: IExtensionDescription, id: string, name: string, userDescription: string, modelDescription: string | undefined, resolver: vscode.ChatVariableResolver): IDisposable { + registerVariableResolver(extension: IExtensionDescription, id: string, name: string, userDescription: string, modelDescription: string | undefined, isSlow: boolean | undefined, resolver: vscode.ChatVariableResolver): IDisposable { const handle = ExtHostChatVariables._idPool++; this._resolver.set(handle, { extension, data: { id, name, description: userDescription, modelDescription }, resolver: resolver }); - this._proxy.$registerVariable(handle, { id, name, description: userDescription, modelDescription }); + this._proxy.$registerVariable(handle, { id, name, description: userDescription, modelDescription, isSlow }); return toDisposable(() => { this._resolver.delete(handle); diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts index 8e036b5f016..da4a7aea5f5 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts @@ -362,10 +362,14 @@ class VariableCompletions extends Disposable { return null; } + const usedAgent = widget.parsedInput.parts.find(p => p instanceof ChatRequestAgentPart); + const slowSupported = usedAgent ? usedAgent.agent.metadata.supportsSlowVariables : true; + const usedVariables = widget.parsedInput.parts.filter((p): p is ChatRequestVariablePart => p instanceof ChatRequestVariablePart); const variableItems = Array.from(this.chatVariablesService.getVariables()) // This doesn't look at dynamic variables like `file`, where multiple makes sense. .filter(v => !usedVariables.some(usedVar => usedVar.variableName === v.name)) + .filter(v => !v.isSlow || slowSupported) .map((v): CompletionItem => { const withLeader = `${chatVariableLeader}${v.name}`; return { diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index e6af8717dc5..9b48a3f2529 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -110,6 +110,7 @@ export interface IChatAgentMetadata { followupPlaceholder?: string; isSticky?: boolean; requester?: IChatRequesterInformation; + supportsSlowVariables?: boolean; } diff --git a/src/vs/workbench/contrib/chat/common/chatRequestParser.ts b/src/vs/workbench/contrib/chat/common/chatRequestParser.ts index 6df98cbdaaf..73276b9464d 100644 --- a/src/vs/workbench/contrib/chat/common/chatRequestParser.ts +++ b/src/vs/workbench/contrib/chat/common/chatRequestParser.ts @@ -147,11 +147,13 @@ export class ChatRequestParser { const variableArg = nextVariableMatch[2] ?? ''; const varRange = new OffsetRange(offset, offset + full.length); const varEditorRange = new Range(position.lineNumber, position.column, position.lineNumber, position.column + full.length); + const usedAgent = parts.find((p): p is ChatRequestAgentPart => p instanceof ChatRequestAgentPart); + const allowSlow = !usedAgent || usedAgent.agent.metadata.supportsSlowVariables; - if (this.variableService.hasVariable(name)) { - // TODO - not really handling duplicate variables names yet - const id = this.variableService.getVariable(name)!.id ?? ''; - return new ChatRequestVariablePart(varRange, varEditorRange, name, variableArg, id); + // TODO - not really handling duplicate variables names yet + const variable = this.variableService.getVariable(name); + if (variable && (!variable.isSlow || allowSlow)) { + return new ChatRequestVariablePart(varRange, varEditorRange, name, variableArg, variable.id); } return; diff --git a/src/vs/workbench/contrib/chat/common/chatVariables.ts b/src/vs/workbench/contrib/chat/common/chatVariables.ts index 5cdc87bf082..602b899c56c 100644 --- a/src/vs/workbench/contrib/chat/common/chatVariables.ts +++ b/src/vs/workbench/contrib/chat/common/chatVariables.ts @@ -18,6 +18,7 @@ export interface IChatVariableData { name: string; description: string; modelDescription?: string; + isSlow?: boolean; hidden?: boolean; canTakeArgument?: boolean; } diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index 13d98419f84..ddd65ccbb54 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -47,6 +47,11 @@ declare module 'vscode' { export interface ChatParticipant { onDidPerformAction: Event; supportIssueReporting?: boolean; + + /** + * Temp, support references that are slow to resolve and should be tools rather than references. + */ + supportsSlowReferences?: boolean; } export interface ChatErrorDetails { diff --git a/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts b/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts index b22c77444bd..7e43d3ad546 100644 --- a/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts +++ b/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts @@ -11,10 +11,12 @@ declare module 'vscode' { * Register a variable which can be used in a chat request to any participant. * @param id A unique ID for the variable. * @param name The name of the variable, to be used in the chat input as `#name`. - * @param description A description of the variable for the chat input suggest widget. + * @param userDescription A description of the variable for the chat input suggest widget. + * @param modelDescription A description of the variable for the model. + * @param isSlow Temp, to limit access to '#codebase' which is not a 'reference' and will fit into a tools API later. * @param resolver Will be called to provide the chat variable's value when it is used. */ - export function registerChatVariableResolver(id: string, name: string, userDescription: string, modelDescription: string | undefined, resolver: ChatVariableResolver): Disposable; + export function registerChatVariableResolver(id: string, name: string, userDescription: string, modelDescription: string | undefined, isSlow: boolean | undefined, resolver: ChatVariableResolver): Disposable; } export interface ChatVariableValue { From e61d72417041e4165f2ed14f1e5e645daab0736a Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 15 May 2024 20:36:29 -0700 Subject: [PATCH 218/357] fix terminal chat hint bug (#212851) --- .../chat/browser/terminal.initialHint.contribution.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts index 544366a3bd6..9d746a55b08 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts @@ -103,7 +103,7 @@ export class TerminalInitialHintContribution extends Disposable implements ITerm private _createHint(): void { const instance = this._instance instanceof TerminalInstance ? this._instance : undefined; const commandDetectionCapability = instance?.capabilities.get(TerminalCapability.CommandDetection); - if (!instance || !this._xterm || this._hintWidget || !commandDetectionCapability || !commandDetectionCapability?.hasInput || instance.reconnectionProperties) { + if (!instance || !this._xterm || this._hintWidget || !commandDetectionCapability || commandDetectionCapability?.hasInput || instance.reconnectionProperties) { return; } From 0cd8d6ac480bc5b4e2a2d7f1eb87eaff9f7d47e1 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Wed, 15 May 2024 21:37:33 -0700 Subject: [PATCH 219/357] fix: make chat progress reporter handle immediately available to extension (#212854) --- .../api/common/extHostChatAgents2.ts | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index eb8f742a2e5..455377b75dd 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -33,6 +33,7 @@ class ChatAgentResponseStream { private _isClosed: boolean = false; private _firstProgress: number | undefined; private _apiObject: vscode.ChatResponseStream | undefined; + private progressReporterHandlePool = 0; constructor( private readonly _extension: IExtensionDescription, @@ -68,31 +69,36 @@ class ChatAgentResponseStream { } } - const _report = (progress: IChatProgressDto, task?: (progress: vscode.Progress) => Thenable) => { + const _report = (progress: IChatProgressDto, task?: (progress: vscode.Progress) => Thenable, progressReporterHandle?: number) => { // Measure the time to the first progress update with real markdown content if (typeof this._firstProgress === 'undefined' && 'content' in progress) { this._firstProgress = this._stopWatch.elapsed(); } - this._proxy.$handleProgressChunk(this._request.requestId, progress) - .then((handle) => { - if (handle) { - task?.({ - report: (p) => { + if (progressReporterHandle !== undefined) { + const progressReporterPromise = this._proxy.$handleProgressChunk(this._request.requestId, progress); + const progressReporter = { + report: (p: vscode.ChatResponseWarningPart | vscode.ChatResponseReferencePart) => { + progressReporterPromise?.then((handle) => { + if (handle) { if (extHostTypes.MarkdownString.isMarkdownString(p.value)) { this._proxy.$handleProgressChunk(this._request.requestId, typeConvert.ChatResponseWarningPart.from(p), handle); - return; } else { this._proxy.$handleProgressChunk(this._request.requestId, typeConvert.ChatResponseReferencePart.from(p), handle); } } - }).then((res) => { - if (typeof handle === 'number') { - this._proxy.$handleProgressChunk(this._request.requestId, typeConvert.ChatTaskResult.from(res), handle); - } }); } + }; + + Promise.all([progressReporterPromise, task?.(progressReporter)]).then(([handle, res]) => { + if (handle !== undefined && res !== undefined) { + this._proxy.$handleProgressChunk(this._request.requestId, typeConvert.ChatTaskResult.from(res), handle); + } }); + } else { + this._proxy.$handleProgressChunk(this._request.requestId, progress); + } }; this._apiObject = { @@ -139,7 +145,7 @@ class ChatAgentResponseStream { throwIfDone(this.progress); const part = new extHostTypes.ChatResponseProgressPart2(value, task); const dto = task ? typeConvert.ChatTask.from(part) : typeConvert.ChatResponseProgressPart.from(part); - _report(dto, task); + _report(dto, task, that.progressReporterHandlePool++); return this; }, warning(value) { From 652c948b85ff951ced7178212e6bc914ff8c7aaf Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 15 May 2024 22:03:17 -0700 Subject: [PATCH 220/357] Check for allowed names in the participant fullName (#212849) * Check for allowed names in the participant fullName * Validate provider name and fullName * Fix --- src/vs/base/common/strings.ts | 21 +++++++++++++++++++ src/vs/base/test/common/strings.test.ts | 16 ++++++++++++++ .../contrib/chat/browser/chatAgentHover.ts | 6 +++--- .../chatMarkdownDecorationsRenderer.ts | 2 +- .../browser/chatParticipantContributions.ts | 17 +++++++++++++++ .../browser/contrib/chatInputCompletions.ts | 2 +- .../contrib/chat/common/chatAgents.ts | 16 +++++++++++--- .../contrib/chat/common/chatViewModel.ts | 20 ++++++++++++------ 8 files changed, 86 insertions(+), 14 deletions(-) diff --git a/src/vs/base/common/strings.ts b/src/vs/base/common/strings.ts index 79d22ee0e6d..70bded5a67d 100644 --- a/src/vs/base/common/strings.ts +++ b/src/vs/base/common/strings.ts @@ -1249,6 +1249,16 @@ export class AmbiguousCharacters { return this.confusableDictionary.has(codePoint); } + public containsAmbiguousCharacter(str: string): boolean { + for (let i = 0; i < str.length; i++) { + const codePoint = str.codePointAt(i); + if (typeof codePoint === 'number' && this.isAmbiguous(codePoint)) { + return true; + } + } + return false; + } + /** * Returns the non basic ASCII code point that the given code point can be confused, * or undefined if such code point does note exist. @@ -1281,6 +1291,17 @@ export class InvisibleCharacters { return InvisibleCharacters.getData().has(codePoint); } + public static containsInvisibleCharacter(str: string): boolean { + for (let i = 0; i < str.length; i++) { + const codePoint = str.codePointAt(i); + if (typeof codePoint === 'number' && InvisibleCharacters.isInvisibleCharacter(codePoint)) { + return true; + } + } + return false; + + } + public static get codePoints(): ReadonlySet { return InvisibleCharacters.getData(); } diff --git a/src/vs/base/test/common/strings.test.ts b/src/vs/base/test/common/strings.test.ts index bb9e5db1d51..4be439f4656 100644 --- a/src/vs/base/test/common/strings.test.ts +++ b/src/vs/base/test/common/strings.test.ts @@ -558,6 +558,22 @@ suite('Strings', () => { assert.strictEqual(strings.count('hello world', 'foo'), 0); }); + test('containsAmbiguousCharacter', () => { + assert.strictEqual(strings.AmbiguousCharacters.getInstance(new Set()).containsAmbiguousCharacter('abcd'), false); + assert.strictEqual(strings.AmbiguousCharacters.getInstance(new Set()).containsAmbiguousCharacter('üå'), false); + assert.strictEqual(strings.AmbiguousCharacters.getInstance(new Set()).containsAmbiguousCharacter('(*&^)'), false); + + assert.strictEqual(strings.AmbiguousCharacters.getInstance(new Set()).containsAmbiguousCharacter('ο'), true); + assert.strictEqual(strings.AmbiguousCharacters.getInstance(new Set()).containsAmbiguousCharacter('abɡc'), true); + }); + + test('containsInvisibleCharacter', () => { + assert.strictEqual(strings.InvisibleCharacters.containsInvisibleCharacter('abcd'), false); + assert.strictEqual(strings.InvisibleCharacters.containsInvisibleCharacter(' '), true); + assert.strictEqual(strings.InvisibleCharacters.containsInvisibleCharacter('a\u{e004e}b'), true); + assert.strictEqual(strings.InvisibleCharacters.containsInvisibleCharacter('a\u{e015a}\u000bb'), true); + }); + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts b/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts index 6216dc2248d..7e738c0ccfe 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts @@ -15,7 +15,7 @@ import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IChatAgentData, IChatAgentNameService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { getFullyQualifiedId, IChatAgentData, IChatAgentNameService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { showExtensionsWithIdsCommandId } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { verifiedPublisherIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; @@ -87,7 +87,8 @@ export class ChatAgentHover extends Disposable { this.domNode.classList.toggle('noExtensionName', !!agent.isDynamic); - this.name.textContent = `@${agent.name}`; + const isAllowed = this.chatAgentNameService.getAgentNameRestriction(agent); + this.name.textContent = isAllowed ? `@${agent.name}` : getFullyQualifiedId(agent); this.extensionName.textContent = agent.extensionDisplayName; this.publisherName.textContent = agent.publisherDisplayName ?? agent.extensionPublisherId; @@ -99,7 +100,6 @@ export class ChatAgentHover extends Disposable { } this.description.textContent = description; - const isAllowed = this.chatAgentNameService.getAgentNameRestriction(agent).get(); this.domNode.classList.toggle('allowedName', isAllowed); this.domNode.classList.toggle('verifiedPublisher', false); diff --git a/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts index b62beef0cf4..25c4ac91e44 100644 --- a/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts @@ -40,7 +40,7 @@ export function agentToMarkdown(agent: IChatAgentData, isClickable: boolean, acc const chatAgentNameService = accessor.get(IChatAgentNameService); const chatAgentService = accessor.get(IChatAgentService); - const isAllowed = chatAgentNameService.getAgentNameRestriction(agent).get(); + const isAllowed = chatAgentNameService.getAgentNameRestriction(agent); let name = `${isAllowed ? agent.name : getFullyQualifiedId(agent)}`; const isDupe = isAllowed && chatAgentService.getAgentsByName(agent.name).length > 1; if (isDupe) { diff --git a/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts b/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts index d05ea9d07e5..e9e074f0cfb 100644 --- a/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { isNonEmptyArray } from 'vs/base/common/arrays'; +import * as strings from 'vs/base/common/strings'; import { Codicon } from 'vs/base/common/codicons'; import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { localize, localize2 } from 'vs/nls'; @@ -159,6 +160,22 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { chatParticipantExtensionPoint.setHandler((extensions, delta) => { for (const extension of delta.added) { for (const providerDescriptor of extension.value) { + if (!providerDescriptor.name.match(/^[\w0-9_-]+$/)) { + this.logService.error(`Extension '${extension.description.identifier.value}' CANNOT register participant with invalid name: ${providerDescriptor.name}. Name must match /^[\\w0-9_-]+$/.`); + continue; + } + + if (providerDescriptor.fullName && strings.AmbiguousCharacters.getInstance(new Set()).containsAmbiguousCharacter(providerDescriptor.fullName)) { + this.logService.error(`Extension '${extension.description.identifier.value}' CANNOT register participant with fullName that contains ambiguous characters: ${providerDescriptor.fullName}.`); + continue; + } + + // Spaces are allowed but considered "invisible" + if (providerDescriptor.fullName && strings.InvisibleCharacters.containsInvisibleCharacter(providerDescriptor.fullName.replace(/ /g, ''))) { + this.logService.error(`Extension '${extension.description.identifier.value}' CANNOT register participant with fullName that contains invisible characters: ${providerDescriptor.fullName}.`); + continue; + } + if (providerDescriptor.isDefault && !isProposedApiEnabled(extension.description, 'defaultChatParticipant')) { this.logService.error(`Extension '${extension.description.identifier.value}' CANNOT use API proposal: defaultChatParticipant.`); continue; diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts index da4a7aea5f5..5e0dc593588 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts @@ -393,7 +393,7 @@ class VariableCompletions extends Disposable { Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(VariableCompletions, LifecyclePhase.Eventually); function getAgentCompletionDetails(agent: IChatAgentData, otherAgents: IChatAgentData[], chatAgentNameService: IChatAgentNameService): { label: string; isDupe: boolean } { - const isAllowed = chatAgentNameService.getAgentNameRestriction(agent).get(); + const isAllowed = chatAgentNameService.getAgentNameRestriction(agent); const agentLabel = `${chatAgentLeader}${isAllowed ? agent.name : getFullyQualifiedId(agent)}`; const isDupe = isAllowed && !!otherAgents.find(other => other.name === agent.name && other.id !== agent.id); diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index 9b48a3f2529..3fc918226a5 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -389,7 +389,7 @@ interface IChatParticipantRegistryResponse { export interface IChatAgentNameService { _serviceBrand: undefined; - getAgentNameRestriction(chatAgentData: IChatAgentData): IObservable; + getAgentNameRestriction(chatAgentData: IChatAgentData): boolean; } export class ChatAgentNameService implements IChatAgentNameService { @@ -454,10 +454,20 @@ export class ChatAgentNameService implements IChatAgentNameService { this.storageService.store(ChatAgentNameService.StorageKey, JSON.stringify(registry), StorageScope.APPLICATION, StorageTarget.MACHINE); } - getAgentNameRestriction(chatAgentData: IChatAgentData): IObservable { + /** + * Returns true if the agent is allowed to use this name + */ + getAgentNameRestriction(chatAgentData: IChatAgentData): boolean { + // TODO would like to use observables here but nothing uses it downstream and I'm not sure how to combine these two + const nameAllowed = this.checkAgentNameRestriction(chatAgentData.name, chatAgentData).get(); + const fullNameAllowed = !chatAgentData.fullName || this.checkAgentNameRestriction(chatAgentData.fullName.replace(/\s/g, ''), chatAgentData).get(); + return nameAllowed && fullNameAllowed; + } + + private checkAgentNameRestriction(name: string, chatAgentData: IChatAgentData): IObservable { // Registry is a map of name to an array of extension publisher IDs or extension IDs that are allowed to use it. // Look up the list of extensions that are allowed to use this name - const allowList = this.registry.map(registry => registry[chatAgentData.name.toLowerCase()]); + const allowList = this.registry.map(registry => registry[name.toLowerCase()]); return allowList.map(allowList => { if (!allowList) { return true; diff --git a/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/chatViewModel.ts index 8d4161ff747..130ef76979b 100644 --- a/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatViewModel.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; import { Disposable } from 'vs/base/common/lifecycle'; import { marked } from 'vs/base/common/marked/marked'; import { ThemeIcon } from 'vs/base/common/themables'; @@ -11,13 +12,12 @@ import { URI } from 'vs/base/common/uri'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { annotateVulnerabilitiesInText } from 'vs/workbench/contrib/chat/common/annotations'; -import { IChatAgentCommand, IChatAgentData, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { getFullyQualifiedId, IChatAgentCommand, IChatAgentData, IChatAgentNameService, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatModelInitState, IChatModel, IChatRequestModel, IChatResponseModel, IChatTextEditGroup, IChatWelcomeMessageContent, IResponse } from 'vs/workbench/contrib/chat/common/chatModel'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { IChatCommandButton, IChatConfirmation, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseErrorDetails, IChatResponseProgressFileTreeData, IChatTask, IChatUsedContext, IChatWarningMessage, ChatAgentVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { ChatAgentVoteDirection, IChatCommandButton, IChatConfirmation, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseErrorDetails, IChatResponseProgressFileTreeData, IChatTask, IChatUsedContext, IChatWarningMessage } from 'vs/workbench/contrib/chat/common/chatService'; import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; import { CodeBlockModelCollection } from './codeBlockModelCollection'; -import { IMarkdownString } from 'vs/base/common/htmlContent'; export function isRequestVM(item: unknown): item is IChatRequestViewModel { return !!item && typeof item === 'object' && 'message' in item; @@ -369,9 +369,16 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi } get username() { - return this.agent ? - (this.agent.fullName || this.agent.name) : - this._model.username; + if (this.agent) { + const isAllowed = this.chatAgentNameService.getAgentNameRestriction(this.agent); + if (isAllowed) { + return this.agent.fullName || this.agent.name; + } else { + return getFullyQualifiedId(this.agent); + } + } + + return this._model.username; } get avatarIcon() { @@ -471,6 +478,7 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi constructor( private readonly _model: IChatResponseModel, @ILogService private readonly logService: ILogService, + @IChatAgentNameService private readonly chatAgentNameService: IChatAgentNameService, ) { super(); From c72bcdaa99d91323bfd29e2ea1df2dba5636a908 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 16 May 2024 09:04:32 +0200 Subject: [PATCH 221/357] Fix split editor inactive group action (#212837) * Fix split editor inactive group action * fix getCommandsContext --- .../browser/parts/editor/editorActions.ts | 6 +- .../browser/parts/editor/editorCommands.ts | 64 +++++++++++-------- src/vs/workbench/common/editor.ts | 6 ++ 3 files changed, 48 insertions(+), 28 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index 703a512413f..4ffd4ae7049 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -61,11 +61,11 @@ abstract class AbstractSplitEditorAction extends Action2 { return preferredSideBySideGroupDirection(configurationService); } - override async run(accessor: ServicesAccessor, context?: IEditorIdentifier): Promise { + override async run(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): Promise { const editorGroupService = accessor.get(IEditorGroupsService); const configurationService = accessor.get(IConfigurationService); - splitEditor(editorGroupService, this.getDirection(configurationService), [context]); + splitEditor(editorGroupService, this.getDirection(configurationService), [getCommandsContext(accessor, resourceOrContext, context)]); } } @@ -1146,7 +1146,7 @@ export class ToggleMaximizeEditorGroupAction extends Action2 { override async run(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): Promise { const editorGroupsService = accessor.get(IEditorGroupsService); - const { group } = resolveCommandsContext(editorGroupsService, getCommandsContext(resourceOrContext, context)); + const { group } = resolveCommandsContext(editorGroupsService, getCommandsContext(accessor, resourceOrContext, context)); editorGroupsService.toggleMaximizeGroup(group); } } diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index b38ddb63f17..37439353b87 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -9,7 +9,7 @@ import { coalesce, distinct } from 'vs/base/common/arrays'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Schemas, matchesScheme } from 'vs/base/common/network'; -import { extname } from 'vs/base/common/resources'; +import { extname, isEqual } from 'vs/base/common/resources'; import { isNumber, isObject, isString, isUndefined } from 'vs/base/common/types'; import { URI, UriComponents } from 'vs/base/common/uri'; import { isDiffEditor } from 'vs/editor/browser/editorBrowser'; @@ -31,7 +31,7 @@ import { ActiveGroupEditorsByMostRecentlyUsedQuickAccess } from 'vs/workbench/br import { SideBySideEditor } from 'vs/workbench/browser/parts/editor/sideBySideEditor'; import { TextDiffEditor } from 'vs/workbench/browser/parts/editor/textDiffEditor'; import { ActiveEditorCanSplitInGroupContext, ActiveEditorGroupEmptyContext, ActiveEditorGroupLockedContext, ActiveEditorStickyContext, MultipleEditorGroupsContext, SideBySideEditorActiveContext, TextCompareEditorActiveContext } from 'vs/workbench/common/contextkeys'; -import { CloseDirection, EditorInputCapabilities, EditorsOrder, IEditorCommandsContext, IEditorIdentifier, IResourceDiffEditorInput, IUntitledTextResourceEditorInput, IVisibleEditorPane, isEditorIdentifier, isEditorInputWithOptionsAndGroup } from 'vs/workbench/common/editor'; +import { CloseDirection, EditorInputCapabilities, EditorsOrder, IEditorCommandsContext, IEditorIdentifier, IResourceDiffEditorInput, IUntitledTextResourceEditorInput, IVisibleEditorPane, isEditorCommandsContext, isEditorIdentifier, isEditorInputWithOptionsAndGroup } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; @@ -656,13 +656,24 @@ function registerFocusEditorGroupAtIndexCommands(): void { } } -export function splitEditor(editorGroupService: IEditorGroupsService, direction: GroupDirection, contexts?: (IEditorCommandsContext | undefined)[]): void { +export function splitEditor(editorGroupService: IEditorGroupsService, direction: GroupDirection, contexts?: (IEditorCommandsContext | URI | undefined)[]): void { let newGroup: IEditorGroup | undefined; for (const context of contexts ?? [undefined]) { let sourceGroup: IEditorGroup | undefined; - if (context && typeof context.groupId === 'number') { + + const isEditorCommand = context && isEditorCommandsContext(context); + const isURI = context && URI.isUri(context); + + if (isEditorCommand) { sourceGroup = editorGroupService.getGroup(context.groupId); + } else if (isURI) { + for (const group of editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE)) { + if (isEqual(group.activeEditor?.resource, context)) { + sourceGroup = group; + break; + } + } } else { sourceGroup = editorGroupService.activeGroup; } @@ -678,7 +689,7 @@ export function splitEditor(editorGroupService: IEditorGroupsService, direction: // Split editor (if it can be split) let editorToCopy: EditorInput | undefined; - if (context && typeof context.editorIndex === 'number') { + if (isEditorCommand && typeof context.editorIndex === 'number') { editorToCopy = sourceGroup.getEditorByIndex(context.editorIndex); } else { editorToCopy = sourceGroup.activeEditor ?? undefined; @@ -686,7 +697,7 @@ export function splitEditor(editorGroupService: IEditorGroupsService, direction: // Copy the editor to the new group, else create an empty group if (editorToCopy && !editorToCopy.hasCapability(EditorInputCapabilities.Singleton)) { - sourceGroup.copyEditor(editorToCopy, newGroup, { preserveFocus: context?.preserveFocus }); + sourceGroup.copyEditor(editorToCopy, newGroup, { preserveFocus: isEditorCommand && context?.preserveFocus }); } } @@ -800,7 +811,7 @@ function registerCloseEditorCommands() { win: { primary: KeyMod.CtrlCmd | KeyCode.F4, secondary: [KeyMod.CtrlCmd | KeyCode.KeyW] }, handler: (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { const editorGroupService = accessor.get(IEditorGroupsService); - const commandsContext = getCommandsContext(resourceOrContext, context); + const commandsContext = getCommandsContext(accessor, resourceOrContext, context); let group: IEditorGroup | undefined; if (commandsContext && typeof commandsContext.groupId === 'number') { @@ -865,7 +876,7 @@ function registerCloseEditorCommands() { handler: async (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { const editorGroupService = accessor.get(IEditorGroupsService); - const { group, editor } = resolveCommandsContext(editorGroupService, getCommandsContext(resourceOrContext, context)); + const { group, editor } = resolveCommandsContext(editorGroupService, getCommandsContext(accessor, resourceOrContext, context)); if (group && editor) { if (group.activeEditor) { group.pinEditor(group.activeEditor); @@ -957,7 +968,7 @@ function registerCloseEditorCommands() { CommandsRegistry.registerCommand(CLOSE_EDITORS_AND_GROUP_COMMAND_ID, async (accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { const editorGroupService = accessor.get(IEditorGroupsService); - const { group } = resolveCommandsContext(editorGroupService, getCommandsContext(resourceOrContext, context)); + const { group } = resolveCommandsContext(editorGroupService, getCommandsContext(accessor, resourceOrContext, context)); if (group) { await group.closeAllEditors(); @@ -1005,7 +1016,7 @@ function registerSplitEditorInGroupCommands(): void { const editorGroupService = accessor.get(IEditorGroupsService); const instantiationService = accessor.get(IInstantiationService); - const { group, editor } = resolveCommandsContext(editorGroupService, getCommandsContext(resourceOrContext, context)); + const { group, editor } = resolveCommandsContext(editorGroupService, getCommandsContext(accessor, resourceOrContext, context)); if (!editor) { return; } @@ -1040,7 +1051,7 @@ function registerSplitEditorInGroupCommands(): void { async function joinEditorInGroup(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): Promise { const editorGroupService = accessor.get(IEditorGroupsService); - const { group, editor } = resolveCommandsContext(editorGroupService, getCommandsContext(resourceOrContext, context)); + const { group, editor } = resolveCommandsContext(editorGroupService, getCommandsContext(accessor, resourceOrContext, context)); if (!(editor instanceof SideBySideEditorInput)) { return; } @@ -1096,7 +1107,7 @@ function registerSplitEditorInGroupCommands(): void { async run(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): Promise { const editorGroupService = accessor.get(IEditorGroupsService); - const { editor } = resolveCommandsContext(editorGroupService, getCommandsContext(resourceOrContext, context)); + const { editor } = resolveCommandsContext(editorGroupService, getCommandsContext(accessor, resourceOrContext, context)); if (editor instanceof SideBySideEditorInput) { await joinEditorInGroup(accessor, resourceOrContext, context); } else if (editor) { @@ -1217,7 +1228,7 @@ function registerOtherEditorCommands(): void { handler: async (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { const editorGroupService = accessor.get(IEditorGroupsService); - const { group, editor } = resolveCommandsContext(editorGroupService, getCommandsContext(resourceOrContext, context)); + const { group, editor } = resolveCommandsContext(editorGroupService, getCommandsContext(accessor, resourceOrContext, context)); if (group && editor) { return group.pinEditor(editor); } @@ -1238,7 +1249,7 @@ function registerOtherEditorCommands(): void { function setEditorGroupLock(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext, locked?: boolean): void { const editorGroupService = accessor.get(IEditorGroupsService); - const { group } = resolveCommandsContext(editorGroupService, getCommandsContext(resourceOrContext, context)); + const { group } = resolveCommandsContext(editorGroupService, getCommandsContext(accessor, resourceOrContext, context)); group?.lock(locked ?? !group.isLocked); } @@ -1346,7 +1357,7 @@ function registerOtherEditorCommands(): void { const editorGroupService = accessor.get(IEditorGroupsService); const quickInputService = accessor.get(IQuickInputService); - const commandsContext = getCommandsContext(resourceOrContext, context); + const commandsContext = getCommandsContext(accessor, resourceOrContext, context); if (commandsContext && typeof commandsContext.groupId === 'number') { const group = editorGroupService.getGroup(commandsContext.groupId); if (group) { @@ -1363,7 +1374,7 @@ function getEditorsContext(accessor: ServicesAccessor, resourceOrContext?: URI | const editorGroupService = accessor.get(IEditorGroupsService); const listService = accessor.get(IListService); - const editorContext = getMultiSelectedEditorContexts(getCommandsContext(resourceOrContext, context), listService, editorGroupService); + const editorContext = getMultiSelectedEditorContexts(getCommandsContext(accessor, resourceOrContext, context), listService, editorGroupService); const activeGroup = editorGroupService.activeGroup; if (editorContext.length === 0 && activeGroup.activeEditor) { @@ -1398,17 +1409,20 @@ export function getEditorsFromContext(accessor: ServicesAccessor, resourceOrCont return editorsAndGroup.filter(group => !!group); } -export function getCommandsContext(resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): IEditorCommandsContext | undefined { - if (URI.isUri(resourceOrContext)) { - return context; +export function getCommandsContext(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): IEditorCommandsContext | undefined { + const isUri = URI.isUri(resourceOrContext); + + const editorCommandsContext = isUri ? context : resourceOrContext; + if (editorCommandsContext && typeof editorCommandsContext.groupId === 'number') { + return editorCommandsContext; } - if (resourceOrContext && typeof resourceOrContext.groupId === 'number') { - return resourceOrContext; - } - - if (context && typeof context.groupId === 'number') { - return context; + if (isUri) { + const editorGroupService = accessor.get(IEditorGroupsService); + const editorGroup = editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE).find(group => isEqual(group.activeEditor?.resource, resourceOrContext)); + if (editorGroup) { + return { groupId: editorGroup.index, editorIndex: editorGroup.getIndexOfEditor(editorGroup.activeEditor!) }; + } } return undefined; diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index 7d75370f67d..91af41524fe 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -1079,6 +1079,12 @@ export interface IEditorCommandsContext { preserveFocus?: boolean; } +export function isEditorCommandsContext(context: unknown): context is IEditorCommandsContext { + const candidate = context as IEditorCommandsContext | undefined; + + return typeof candidate?.groupId === 'number'; +} + /** * More information around why an editor was closed in the model. */ From 6af31616a75e6e4e79911891fa0eb75b09ed523d Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 16 May 2024 15:48:04 +0200 Subject: [PATCH 222/357] macOS/Linux: Allow `\` in file names (fix #212740) (#212810) * macOS/Linux: Allow `\` in file names (fix #212740) * fix tests * bump salt --- build/.cachesalt | 2 +- src/vs/base/common/extpath.ts | 2 +- src/vs/base/test/common/extpath.test.ts | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/build/.cachesalt b/build/.cachesalt index 8051d84124e..8148922f086 100644 --- a/build/.cachesalt +++ b/build/.cachesalt @@ -1 +1 @@ -2024-03-18T08:47:22.277Z +2024-05-16T08:47:22.277Z diff --git a/src/vs/base/common/extpath.ts b/src/vs/base/common/extpath.ts index d7f6723092c..e0ee6968dce 100644 --- a/src/vs/base/common/extpath.ts +++ b/src/vs/base/common/extpath.ts @@ -164,7 +164,7 @@ export function isUNC(path: string): boolean { // Reference: https://en.wikipedia.org/wiki/Filename const WINDOWS_INVALID_FILE_CHARS = /[\\/:\*\?"<>\|]/g; -const UNIX_INVALID_FILE_CHARS = /[\\/]/g; +const UNIX_INVALID_FILE_CHARS = /[/]/g; const WINDOWS_FORBIDDEN_NAMES = /^(con|prn|aux|clock\$|nul|lpt[0-9]|com[0-9])(\.(.*?))?$/i; export function isValidBasename(name: string | null | undefined, isWindowsOS: boolean = isWindows): boolean { const invalidFileChars = isWindowsOS ? WINDOWS_INVALID_FILE_CHARS : UNIX_INVALID_FILE_CHARS; diff --git a/src/vs/base/test/common/extpath.test.ts b/src/vs/base/test/common/extpath.test.ts index 3c6a4e4979b..c13210daa13 100644 --- a/src/vs/base/test/common/extpath.test.ts +++ b/src/vs/base/test/common/extpath.test.ts @@ -50,9 +50,9 @@ suite('Paths', () => { assert.ok(!extpath.isValidBasename('')); assert.ok(extpath.isValidBasename('test.txt')); assert.ok(!extpath.isValidBasename('/test.txt')); - assert.ok(!extpath.isValidBasename('\\test.txt')); if (isWindows) { + assert.ok(!extpath.isValidBasename('\\test.txt')); assert.ok(!extpath.isValidBasename('aux')); assert.ok(!extpath.isValidBasename('Aux')); assert.ok(!extpath.isValidBasename('LPT0')); @@ -69,6 +69,8 @@ suite('Paths', () => { assert.ok(!extpath.isValidBasename('test.txt\t')); assert.ok(!extpath.isValidBasename('tes:t.txt')); assert.ok(!extpath.isValidBasename('tes"t.txt')); + } else { + assert.ok(extpath.isValidBasename('\\test.txt')); } }); From 146b3fb6e02244f991647ecfc34df54b560d7b9c Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 16 May 2024 16:15:28 +0200 Subject: [PATCH 223/357] Update tab selected background color (#212872) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tab selected background light vs Co-authored-by: João Moreno --- extensions/theme-defaults/themes/light_vs.json | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/theme-defaults/themes/light_vs.json b/extensions/theme-defaults/themes/light_vs.json index 50a315348f7..e4cc701f82c 100644 --- a/extensions/theme-defaults/themes/light_vs.json +++ b/extensions/theme-defaults/themes/light_vs.json @@ -24,6 +24,7 @@ "sideBarSectionHeader.background": "#0000", "sideBarSectionHeader.border": "#61616130", "tab.selectedForeground": "#333333b3", + "tab.selectedBackground": "#ffffffa5", "tab.lastPinnedBorder": "#61616130", "notebook.cellBorderColor": "#E8E8E8", "notebook.selectedCellBackground": "#c8ddf150", From 95978d5bfb0fce1920e194d38790b1c53285e377 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 16 May 2024 16:18:20 +0200 Subject: [PATCH 224/357] fixes https://github.com/microsoft/vscode-copilot/issues/4599 (#212870) --- .../contentWidgets/contentWidgets.ts | 51 ++++++++++++++++--- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts b/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts index 3112c19c324..a11435b8e73 100644 --- a/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts +++ b/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts @@ -167,11 +167,19 @@ interface IBoxLayoutResult { left: number; } -interface IRenderData { +interface IOffViewportRenderData { + kind: 'offViewport'; + preserveFocus: boolean; +} + +interface IInViewportRenderData { + kind: 'inViewport'; coordinate: Coordinate; position: ContentWidgetPositionPreference; } +type IRenderData = IInViewportRenderData | IOffViewportRenderData; + class Widget { private readonly _context: ViewContext; private readonly _viewDomNode: FastDomNode; @@ -435,7 +443,11 @@ class Widget { const { primary, secondary } = this._getAnchorsCoordinates(ctx); if (!primary) { - return null; + return { + kind: 'offViewport', + preserveFocus: this.domNode.domNode.contains(this.domNode.domNode.ownerDocument.activeElement) + }; + // return null; } if (this._cachedDomNodeOffsetWidth === -1 || this._cachedDomNodeOffsetHeight === -1) { @@ -474,7 +486,11 @@ class Widget { return null; } if (pass === 2 || placement.fitsAbove) { - return { coordinate: new Coordinate(placement.aboveTop, placement.left), position: ContentWidgetPositionPreference.ABOVE }; + return { + kind: 'inViewport', + coordinate: new Coordinate(placement.aboveTop, placement.left), + position: ContentWidgetPositionPreference.ABOVE + }; } } else if (pref === ContentWidgetPositionPreference.BELOW) { if (!placement) { @@ -482,13 +498,25 @@ class Widget { return null; } if (pass === 2 || placement.fitsBelow) { - return { coordinate: new Coordinate(placement.belowTop, placement.left), position: ContentWidgetPositionPreference.BELOW }; + return { + kind: 'inViewport', + coordinate: new Coordinate(placement.belowTop, placement.left), + position: ContentWidgetPositionPreference.BELOW + }; } } else { if (this.allowEditorOverflow) { - return { coordinate: this._prepareRenderWidgetAtExactPositionOverflowing(new Coordinate(anchor.top, anchor.left)), position: ContentWidgetPositionPreference.EXACT }; + return { + kind: 'inViewport', + coordinate: this._prepareRenderWidgetAtExactPositionOverflowing(new Coordinate(anchor.top, anchor.left)), + position: ContentWidgetPositionPreference.EXACT + }; } else { - return { coordinate: new Coordinate(anchor.top, anchor.left), position: ContentWidgetPositionPreference.EXACT }; + return { + kind: 'inViewport', + coordinate: new Coordinate(anchor.top, anchor.left), + position: ContentWidgetPositionPreference.EXACT + }; } } } @@ -518,12 +546,19 @@ class Widget { } public render(ctx: RestrictedRenderingContext): void { - if (!this._renderData) { + if (!this._renderData || this._renderData.kind === 'offViewport') { // This widget should be invisible if (this._isVisible) { this.domNode.removeAttribute('monaco-visible-content-widget'); this._isVisible = false; - this.domNode.setVisibility('hidden'); + + if (this._renderData?.kind === 'offViewport' && this._renderData.preserveFocus) { + // widget wants to be shown, but it is outside of the viewport and it + // has focus which we need to preserve + this.domNode.setTop(-1000); + } else { + this.domNode.setVisibility('hidden'); + } } if (typeof this._actual.afterRender === 'function') { From 88f27ceb0c235a4e84f3e4271f39305961f150bd Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 16 May 2024 16:18:23 +0200 Subject: [PATCH 225/357] disable `vscode.lm`-API for stable (#212890) https://github.com/microsoft/vscode/issues/206265 --- src/vs/workbench/api/common/extHost.api.impl.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index cdf73ee943e..3b3c6f87b22 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -14,7 +14,7 @@ import { TextEditorCursorStyle } from 'vs/editor/common/config/editorOptions'; import { score, targetsNotebooks } from 'vs/editor/common/languageSelector'; import * as languageConfiguration from 'vs/editor/common/languages/languageConfiguration'; import { OverviewRulerLane } from 'vs/editor/common/model'; -import { ExtensionIdentifierSet, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, ExtensionIdentifierSet, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import * as files from 'vs/platform/files/common/files'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ILogService, ILoggerService, LogLevel } from 'vs/platform/log/common/log'; @@ -1431,9 +1431,17 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I // namespace: lm const lm: typeof vscode.lm = { selectChatModels: (selector) => { + if (initData.quality === 'stable') { + console.warn(`[${ExtensionIdentifier.toKey(extension.identifier)}] This API is disabled in '${initData.environment.appName}'-stable.`); + return Promise.resolve([]); + } return extHostLanguageModels.selectLanguageModels(extension, selector ?? {}); }, onDidChangeChatModels: (listener, thisArgs?, disposables?) => { + if (initData.quality === 'stable') { + console.warn(`[${ExtensionIdentifier.toKey(extension.identifier)}] This API is disabled in '${initData.environment.appName}'-stable.`); + return Event.None(listener, thisArgs, disposables); + } return extHostLanguageModels.onDidChangeProviders(listener, thisArgs, disposables); }, // --- embeddings From 462a2a7c8fcfa063773202dc3c32e6dac67356ee Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 16 May 2024 16:19:53 +0200 Subject: [PATCH 226/357] use agent's full name in the inline chat provider wrapper (#212871) --- .../contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts index df150557ede..5f1e3ac6b3d 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts @@ -761,7 +761,7 @@ export class AgentInlineChatProvider implements IInlineChatSessionProvider { readonly agent: IChatAgent, @IChatAgentService private readonly _chatAgentService: IChatAgentService, ) { - this.label = agent.name; + this.label = agent.fullName ?? agent.name; this.extensionId = agent.extensionId; this.supportIssueReporting = agent.metadata.supportIssueReporting; } From bb684610f34c4a6c239f9ecd024d6a403f6c4f6b Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 16 May 2024 16:29:57 +0200 Subject: [PATCH 227/357] editors - some :lipstick: from code review (#212886) --- .../browser/parts/editor/editorCommands.ts | 8 +++----- .../browser/parts/editor/editorDropTarget.ts | 16 ++++++++-------- .../browser/parts/editor/editorTabsControl.ts | 4 ++-- .../parts/editor/multiEditorTabsControl.ts | 2 +- .../browser/parts/editor/noEditorTabsControl.ts | 2 +- .../parts/editor/singleEditorTabsControl.ts | 10 +++------- .../workbench/common/editor/editorGroupModel.ts | 14 ++++++++------ .../common/editor/filteredEditorGroupModel.ts | 2 +- .../editor/common/editorGroupsService.ts | 1 - 9 files changed, 27 insertions(+), 32 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index 37439353b87..5122b21b8d0 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -902,11 +902,10 @@ function registerCloseEditorCommands() { for (const { editor, group } of editorsAndGroup) { const untypedEditor = editor.toUntyped(); - - // Resolver can only resolve untyped editors if (!untypedEditor) { - return; + return; // Resolver can only resolve untyped editors } + untypedEditor.options = { ...editorService.activeEditorPane?.options, override: EditorResolution.PICK }; const resolvedEditor = await editorResolverService.resolveEditor(untypedEditor, group); if (!isEditorInputWithOptionsAndGroup(resolvedEditor)) { @@ -925,7 +924,6 @@ function registerCloseEditorCommands() { }); // Telemetry - type WorkbenchEditorReopenClassification = { owner: 'rebornix'; comment: 'Identify how a document is reopened'; @@ -1403,7 +1401,7 @@ export function getEditorsFromContext(accessor: ServicesAccessor, resourceOrCont if (!editor || !group) { return undefined; } - return { editor: editor, group: group }; + return { editor, group }; }); return editorsAndGroup.filter(group => !!group); diff --git a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts index aaa77f5babb..002d565c6cd 100644 --- a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts +++ b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts @@ -165,7 +165,7 @@ class DropOverlay extends Themable { isCopy = this.isCopyOperation(e); } else if (isDraggingEditor) { const data = this.editorTransfer.getData(DraggedEditorIdentifier.prototype); - if (Array.isArray(data)) { + if (Array.isArray(data) && data.length > 0) { isCopy = this.isCopyOperation(e, data[0].identifier); } } @@ -234,7 +234,7 @@ class DropOverlay extends Themable { // Check for group transfer if (this.groupTransfer.hasData(DraggedEditorGroupIdentifier.prototype)) { const data = this.groupTransfer.getData(DraggedEditorGroupIdentifier.prototype); - if (Array.isArray(data)) { + if (Array.isArray(data) && data.length > 0) { return this.editorGroupService.getGroup(data[0].identifier); } } @@ -242,7 +242,7 @@ class DropOverlay extends Themable { // Check for editor transfer else if (this.editorTransfer.hasData(DraggedEditorIdentifier.prototype)) { const data = this.editorTransfer.getData(DraggedEditorIdentifier.prototype); - if (Array.isArray(data)) { + if (Array.isArray(data) && data.length > 0) { return this.editorGroupService.getGroup(data[0].identifier.groupId); } } @@ -267,7 +267,7 @@ class DropOverlay extends Themable { // Check for group transfer if (this.groupTransfer.hasData(DraggedEditorGroupIdentifier.prototype)) { const data = this.groupTransfer.getData(DraggedEditorGroupIdentifier.prototype); - if (Array.isArray(data)) { + if (Array.isArray(data) && data.length > 0) { const sourceGroup = this.editorGroupService.getGroup(data[0].identifier); if (sourceGroup) { if (typeof splitDirection !== 'number' && sourceGroup === this.groupView) { @@ -306,7 +306,7 @@ class DropOverlay extends Themable { // Check for editor transfer else if (this.editorTransfer.hasData(DraggedEditorIdentifier.prototype)) { const data = this.editorTransfer.getData(DraggedEditorIdentifier.prototype); - if (Array.isArray(data)) { + if (Array.isArray(data) && data.length > 0) { const draggedEditors = data; const firstDraggedEditor = data[0].identifier; @@ -333,8 +333,8 @@ class DropOverlay extends Themable { { editor: draggedEditor.identifier.editor, options: fillActiveEditorViewState(sourceGroup, draggedEditor.identifier.editor, { - pinned: true, // always pin dropped editor - sticky: sourceGroup.isSticky(firstDraggedEditor.editor), // preserve sticky state + pinned: true, // always pin dropped editor + sticky: sourceGroup.isSticky(firstDraggedEditor.editor) // preserve sticky state }) } )); @@ -357,7 +357,7 @@ class DropOverlay extends Themable { // Check for tree items else if (this.treeItemsTransfer.hasData(DraggedTreeItemsIdentifier.prototype)) { const data = this.treeItemsTransfer.getData(DraggedTreeItemsIdentifier.prototype); - if (Array.isArray(data)) { + if (Array.isArray(data) && data.length > 0) { const editors: IUntypedEditorInput[] = []; for (const id of data) { const dataTransferItem = await this.treeViewsDragAndDropService.removeDragOperationTransfer(id.identifier); diff --git a/src/vs/workbench/browser/parts/editor/editorTabsControl.ts b/src/vs/workbench/browser/parts/editor/editorTabsControl.ts index 5cbcad63de0..0031e18c565 100644 --- a/src/vs/workbench/browser/parts/editor/editorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/editorTabsControl.ts @@ -86,7 +86,7 @@ export interface IEditorTabsControl extends IDisposable { stickEditor(editor: EditorInput): void; unstickEditor(editor: EditorInput): void; setActive(isActive: boolean): void; - setEditorSelections(editor: EditorInput[], selected: boolean): void; + setEditorSelections(editors: EditorInput[], selected: boolean): void; updateEditorLabel(editor: EditorInput): void; updateEditorDirty(editor: EditorInput): void; layout(dimensions: IEditorTitleControlDimensions): Dimension; @@ -503,7 +503,7 @@ export abstract class EditorTabsControl extends Themable implements IEditorTabsC abstract setActive(isActive: boolean): void; - abstract setEditorSelections(editor: EditorInput[], selected: boolean): void; + abstract setEditorSelections(editors: EditorInput[], selected: boolean): void; abstract updateEditorLabel(editor: EditorInput): void; diff --git a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts index e99641dc2c1..aa6a35fffcd 100644 --- a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts @@ -1535,7 +1535,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { // Borders / outline this.redrawTabBorders(tabIndex, tabContainer); - // Active / dirty state + // Selection / active / dirty state this.redrawTabSelectedActiveAndDirty(this.groupsView.activeGroup === this.groupView, editor, tabContainer, tabActionBar); } diff --git a/src/vs/workbench/browser/parts/editor/noEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/noEditorTabsControl.ts index 68eda55c923..36a8ac0f0c2 100644 --- a/src/vs/workbench/browser/parts/editor/noEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/noEditorTabsControl.ts @@ -61,7 +61,7 @@ export class NoEditorTabsControl extends EditorTabsControl { setActive(isActive: boolean): void { } - setEditorSelections(editor: EditorInput[], selected: boolean): void { } + setEditorSelections(editors: EditorInput[], selected: boolean): void { } updateEditorLabel(editor: EditorInput): void { } diff --git a/src/vs/workbench/browser/parts/editor/singleEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/singleEditorTabsControl.ts index 97f96a7d948..4b4e48163df 100644 --- a/src/vs/workbench/browser/parts/editor/singleEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/singleEditorTabsControl.ts @@ -179,19 +179,15 @@ export class SingleEditorTabsControl extends EditorTabsControl { this.ifEditorIsActive(editor, () => this.redraw()); } - stickEditor(editor: EditorInput): void { - // Sticky editors are not presented any different with tabs disabled - } + stickEditor(editor: EditorInput): void { } - unstickEditor(editor: EditorInput): void { - // Sticky editors are not presented any different with tabs disabled - } + unstickEditor(editor: EditorInput): void { } setActive(isActive: boolean): void { this.redraw(); } - setEditorSelections(editor: EditorInput[], selected: boolean): void { } + setEditorSelections(editors: EditorInput[], selected: boolean): void { } updateEditorLabel(editor: EditorInput): void { this.ifEditorIsActive(editor, () => this.redraw()); diff --git a/src/vs/workbench/common/editor/editorGroupModel.ts b/src/vs/workbench/common/editor/editorGroupModel.ts index ab75d18ab3f..515f3408e10 100644 --- a/src/vs/workbench/common/editor/editorGroupModel.ts +++ b/src/vs/workbench/common/editor/editorGroupModel.ts @@ -221,17 +221,18 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { private locked = false; - private selected: EditorInput[] = []; // editors in selected state, first one is active + private selected: EditorInput[] = []; // editors in selected state, first one is active + private set active(editor: EditorInput | null) { this.selected = editor ? [editor] : []; } private get active(): EditorInput | null { - return this.selected[0] || null; + return this.selected[0] ?? null; } - private preview: EditorInput | null = null; // editor in preview state - private sticky = -1; // index of first editor in sticky state - private readonly transient = new Set(); // editors in transient state + private preview: EditorInput | null = null; // editor in preview state + private sticky = -1; // index of first editor in sticky state + private readonly transient = new Set(); // editors in transient state private editorOpenPositioning: ('left' | 'right' | 'first' | 'last') | undefined; private focusRecentEditorAfterClose: boolean | undefined; @@ -582,6 +583,7 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { this.active = null; } } + // Remove from selection else if (!isActiveEditor) { const wasSelected = !!this.selected.find(selected => this.matches(selected, editor)); @@ -729,7 +731,7 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { return this.editors.filter(editor => this.isSelected(editor)); } - isSelected(editor: number | EditorInput): boolean { + isSelected(editor: EditorInput | number): boolean { if (typeof editor === 'number') { editor = this.editors[editor]; } diff --git a/src/vs/workbench/common/editor/filteredEditorGroupModel.ts b/src/vs/workbench/common/editor/filteredEditorGroupModel.ts index 787ee09f001..fcda0bbed00 100644 --- a/src/vs/workbench/common/editor/filteredEditorGroupModel.ts +++ b/src/vs/workbench/common/editor/filteredEditorGroupModel.ts @@ -42,7 +42,7 @@ abstract class FilteredEditorGroupModel extends Disposable implements IReadonlyE isTransient(editorOrIndex: EditorInput | number): boolean { return this.model.isTransient(editorOrIndex); } isSticky(editorOrIndex: EditorInput | number): boolean { return this.model.isSticky(editorOrIndex); } isActive(editor: EditorInput | IUntypedEditorInput): boolean { return this.model.isActive(editor); } - isSelected(editor: number | EditorInput): boolean { return this.model.isSelected(editor); } + isSelected(editor: EditorInput | number): boolean { return this.model.isSelected(editor); } isFirst(editor: EditorInput): boolean { return this.model.isFirst(editor, this.getEditors(EditorsOrder.SEQUENTIAL)); diff --git a/src/vs/workbench/services/editor/common/editorGroupsService.ts b/src/vs/workbench/services/editor/common/editorGroupsService.ts index cb9c84b7590..6ecc2b3d333 100644 --- a/src/vs/workbench/services/editor/common/editorGroupsService.ts +++ b/src/vs/workbench/services/editor/common/editorGroupsService.ts @@ -779,7 +779,6 @@ export interface IEditorGroup { */ selectEditor(editor: EditorInput, active?: boolean): Promise; - /** * Selects the editors in the group. If activeEditor is provided, * it will be the active editor in the group. From c3b42d517d72bda7bc212f64b11d67524a60cc71 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 16 May 2024 16:32:50 +0200 Subject: [PATCH 228/357] `code --wait` no longer exits when the file is closed in VSCode Insiders (fix #211866) (#212868) --- src/vs/workbench/common/editor/editorGroupModel.ts | 2 +- .../services/textfile/common/textFileEditorModelManager.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/common/editor/editorGroupModel.ts b/src/vs/workbench/common/editor/editorGroupModel.ts index 515f3408e10..146224c838a 100644 --- a/src/vs/workbench/common/editor/editorGroupModel.ts +++ b/src/vs/workbench/common/editor/editorGroupModel.ts @@ -206,7 +206,7 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { //#region events - private readonly _onDidModelChange = this._register(new Emitter()); + private readonly _onDidModelChange = this._register(new Emitter({ leakWarningThreshold: 500 /* increased for users with hundreds of inputs opened */ })); readonly onDidModelChange = this._onDidModelChange.event; //#endregion diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts index ec6d4c95742..24f0d458459 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts @@ -27,7 +27,7 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' export class TextFileEditorModelManager extends Disposable implements ITextFileEditorModelManager { - private readonly _onDidCreate = this._register(new Emitter()); + private readonly _onDidCreate = this._register(new Emitter({ leakWarningThreshold: 500 /* increased for users with hundreds of inputs opened */ })); readonly onDidCreate = this._onDidCreate.event; private readonly _onDidResolve = this._register(new Emitter()); From ed83e0e84f502789f73a78898b75ef09c7a051e4 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 16 May 2024 16:34:30 +0200 Subject: [PATCH 229/357] Remove experimental hover feature (#212865) Remove explorer experimental hover --- .../files/browser/media/explorerviewlet.css | 9 -- .../files/browser/views/explorerViewer.ts | 88 +------------------ 2 files changed, 2 insertions(+), 95 deletions(-) diff --git a/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css b/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css index 3fb7600cc0f..7a3237b22f5 100644 --- a/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css +++ b/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css @@ -9,15 +9,6 @@ height: 100%; } -.explorer-item-hover { - /* -- Must set important as hover overrides the cursor -- */ - cursor: pointer !important; - padding-left: 6px; - height: 22px; - font-size: 13px !important; - user-select: none !important; -} - .explorer-folders-view .monaco-list-row { padding-left: 4px; /* align top level twistie with `Explorer` title label */ } diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index 62e9f635c48..068fde51b80 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -48,7 +48,7 @@ import { ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree' import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; import { ILabelService } from 'vs/platform/label/common/label'; -import { isNumber, isStringArray } from 'vs/base/common/types'; +import { isNumber } from 'vs/base/common/types'; import { IEditableData } from 'vs/workbench/common/views'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; @@ -62,13 +62,9 @@ import { ResourceSet } from 'vs/base/common/map'; import { TernarySearchTree } from 'vs/base/common/ternarySearchTree'; import { defaultInputBoxStyles } from 'vs/platform/theme/browser/defaultStyles'; import { timeout } from 'vs/base/common/async'; -import { IHoverDelegate, IHoverDelegateOptions } from 'vs/base/browser/ui/hover/hoverDelegate'; -import { IHoverService } from 'vs/platform/hover/browser/hover'; -import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { mainWindow } from 'vs/base/browser/window'; import { IExplorerFileContribution, explorerFileContribRegistry } from 'vs/workbench/contrib/files/browser/explorerFileContrib'; -import type { IHoverWidget } from 'vs/base/browser/ui/hover/hover'; export class ExplorerDelegate implements IListVirtualDelegate { @@ -283,81 +279,6 @@ export class FilesRenderer implements ICompressibleTreeRenderer(); readonly onDidChangeActiveDescendant = this._onDidChangeActiveDescendant.event; - private readonly hoverDelegate = new class implements IHoverDelegate { - - private lastHoverHideTime = 0; - private hiddenFromClick = false; - readonly placement = 'element'; - - get delay() { - // Delay implementation borrowed froms src/vs/workbench/browser/parts/statusbar/statusbarPart.ts - if (Date.now() - this.lastHoverHideTime < 500) { - return 0; // show instantly when a hover was recently shown - } - - return this.configurationService.getValue('workbench.hover.delay'); - } - - constructor( - private readonly configurationService: IConfigurationService, - private readonly hoverService: IHoverService - ) { } - - showHover(options: IHoverDelegateOptions, focus?: boolean): IHoverWidget | undefined { - let element: HTMLElement; - if (options.target instanceof HTMLElement) { - element = options.target; - } else { - element = options.target.targetElements[0]; - } - - const tlRow = element.closest('.monaco-tl-row') as HTMLElement | undefined; - const listRow = tlRow?.closest('.monaco-list-row') as HTMLElement | undefined; - - const child = element.querySelector('div.monaco-icon-label-container') as Element | undefined; - const childOfChild = child?.querySelector('span.monaco-icon-name-container') as HTMLElement | undefined; - let overflowed = false; - if (childOfChild && child) { - const width = child.clientWidth; - const childWidth = childOfChild.offsetWidth; - // Check if element is overflowing its parent container - overflowed = width <= childWidth; - } - - // Only count decorations that provide additional info, as hover overing decorations such as git excluded isn't helpful - const hasDecoration = options.content.toString().includes('•'); - // If it's overflowing or has a decoration show the tooltip - overflowed = overflowed || hasDecoration; - - const indentGuideElement = tlRow?.querySelector('.monaco-tl-indent') as HTMLElement | undefined; - if (!indentGuideElement) { - return; - } - - return overflowed ? this.hoverService.showHover({ - ...options, - target: indentGuideElement, - container: listRow, - additionalClasses: ['explorer-item-hover'], - position: { - hoverPosition: HoverPosition.RIGHT, - }, - appearance: { - compact: true, - skipFadeInAnimation: true, - showPointer: false, - } - }, focus) : undefined; - } - - onDidHideHover(): void { - if (!this.hiddenFromClick) { - this.lastHoverHideTime = Date.now(); - } - this.hiddenFromClick = false; - } - }(this.configurationService, this.hoverService); - constructor( container: HTMLElement, private labels: ResourceLabels, @@ -369,7 +290,6 @@ export class FilesRenderer implements ICompressibleTreeRenderer(); @@ -402,8 +322,7 @@ export class FilesRenderer implements ICompressibleTreeRenderer('explorer.experimental.hover'); - const label = templateDisposables.add(this.labels.create(container, { supportHighlights: true, hoverDelegate: experimentalHover ? this.hoverDelegate : undefined })); + const label = templateDisposables.add(this.labels.create(container, { supportHighlights: true })); templateDisposables.add(label.onDidRender(() => { try { if (templateData.currentContext) { @@ -524,11 +443,8 @@ export class FilesRenderer implements ICompressibleTreeRenderer('explorer.experimental.hover'); templateData.contribs.forEach(c => c.setResource(stat.resource)); templateData.label.setResource({ resource: stat.resource, name: label }, { - title: experimentalHover ? isStringArray(label) ? label[0] : label : undefined, fileKind: stat.isRoot ? FileKind.ROOT_FOLDER : stat.isDirectory ? FileKind.FOLDER : FileKind.FILE, extraClasses: realignNestedChildren ? [...extraClasses, 'align-nest-icon-with-parent-icon'] : extraClasses, fileDecorations: this.config.explorer.decorations, From be648f9de9312b4811affc619ad9311bbc947992 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 16 May 2024 16:34:36 +0200 Subject: [PATCH 230/357] voice - fix synthesis controller for terminal (#212862) --- .../actions/voiceChatActions.ts | 46 +++++++++++++------ 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts index 3a16b271500..c2a0603c397 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts @@ -693,27 +693,45 @@ interface IChatSynthesizerSessionController { class ChatSynthesizerSessionController { static create(accessor: ServicesAccessor, context: IVoiceChatSessionController | 'focused', response: IChatResponseModel): IChatSynthesizerSessionController { - const chatWidgetService = accessor.get(IChatWidgetService); - const contextKeyService = accessor.get(IContextKeyService); - if (context === 'focused') { - let chatWidget = chatWidgetService.getWidgetBySessionId(response.session.sessionId); - if (chatWidget?.location === ChatAgentLocation.Editor) { - // TODO@bpasero workaround for https://github.com/microsoft/vscode/issues/212785 - // but should find a better way how to get to the chat widget from a response - chatWidget = chatWidgetService.lastFocusedWidget; - } - + return ChatSynthesizerSessionController.doCreateForFocusedChat(accessor, response); + } else { return { - onDidHideChat: chatWidget?.onDidHide ?? Event.None, - contextKeyService: chatWidget?.scopedContextKeyService ?? contextKeyService, + onDidHideChat: context.onDidHideInput, + contextKeyService: context.scopedContextKeyService, response }; } + } + + private static doCreateForFocusedChat(accessor: ServicesAccessor, response: IChatResponseModel): IChatSynthesizerSessionController { + const chatWidgetService = accessor.get(IChatWidgetService); + const contextKeyService = accessor.get(IContextKeyService); + const terminalService = accessor.get(ITerminalService); + + // 1.) probe terminal chat which is not part of chat widget service + const activeInstance = terminalService.activeInstance; + if (activeInstance) { + const terminalChat = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + if (terminalChat?.hasFocus()) { + return { + onDidHideChat: terminalChat.onDidHide, + contextKeyService: terminalChat.scopedContextKeyService, + response + }; + } + } + + // 2.) otherwise go via chat widget service + let chatWidget = chatWidgetService.getWidgetBySessionId(response.session.sessionId); + if (chatWidget?.location === ChatAgentLocation.Editor) { + // TODO@bpasero workaround for https://github.com/microsoft/vscode/issues/212785 + chatWidget = chatWidgetService.lastFocusedWidget; + } return { - onDidHideChat: context.onDidHideInput, - contextKeyService: context.scopedContextKeyService, + onDidHideChat: chatWidget?.onDidHide ?? Event.None, + contextKeyService: chatWidget?.scopedContextKeyService ?? contextKeyService, response }; } From da020b932a9ccb89b703b7e03f10633c96f25bb3 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 16 May 2024 16:42:15 +0200 Subject: [PATCH 231/357] Fix editor command context extraction (#212897) Fix context key extraction --- src/vs/workbench/browser/parts/editor/editorCommands.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index 5122b21b8d0..848096c7358 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -1410,7 +1410,7 @@ export function getEditorsFromContext(accessor: ServicesAccessor, resourceOrCont export function getCommandsContext(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): IEditorCommandsContext | undefined { const isUri = URI.isUri(resourceOrContext); - const editorCommandsContext = isUri ? context : resourceOrContext; + const editorCommandsContext = isUri ? context : resourceOrContext ? resourceOrContext : context; if (editorCommandsContext && typeof editorCommandsContext.groupId === 'number') { return editorCommandsContext; } From 660fe6087e67f559beffc91e20a281e1563e2c1c Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 16 May 2024 17:00:08 +0200 Subject: [PATCH 232/357] Add docs and register some child insta services for disposal (#212880) https://github.com/microsoft/vscode/issues/212879 --- src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts | 2 +- src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts | 4 ++-- .../widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts | 4 ++-- src/vs/platform/instantiation/common/instantiation.ts | 6 +++++- .../platform/instantiation/common/instantiationService.ts | 6 ++++-- src/vs/workbench/browser/parts/compositePart.ts | 1 + src/vs/workbench/contrib/chat/browser/chatWidget.ts | 2 +- .../contrib/inlineChat/browser/inlineChatContentWidget.ts | 3 ++- .../contrib/inlineChat/browser/inlineChatWidget.ts | 3 ++- 9 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts index 69bf235e7cb..07753688f7e 100644 --- a/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts @@ -289,7 +289,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE this._register(new EditorContextKeysManager(this, this._contextKeyService)); this._register(new EditorModeContext(this, this._contextKeyService, languageFeaturesService)); - this._instantiationService = instantiationService.createChild(new ServiceCollection([IContextKeyService, this._contextKeyService])); + this._instantiationService = this._register(instantiationService.createChild(new ServiceCollection([IContextKeyService, this._contextKeyService]))); this._modelData = null; diff --git a/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts index 5fdc32e47ee..cd5c2e8b606 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts @@ -68,9 +68,9 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { public get onDidContentSizeChange() { return this._editors.onDidContentSizeChange; } private readonly _contextKeyService = this._register(this._parentContextKeyService.createScoped(this._domElement)); - private readonly _instantiationService = this._parentInstantiationService.createChild( + private readonly _instantiationService = this._register(this._parentInstantiationService.createChild( new ServiceCollection([IContextKeyService, this._contextKeyService]) - ); + )); private readonly _rootSizeObserver: ObservableElementSizeObserver; diff --git a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts index 82343308057..c29fc74bddd 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts +++ b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts @@ -114,9 +114,9 @@ export class MultiDiffEditorWidgetImpl extends Disposable { }); private readonly _contextKeyService = this._register(this._parentContextKeyService.createScoped(this._element)); - private readonly _instantiationService = this._parentInstantiationService.createChild( + private readonly _instantiationService = this._register(this._parentInstantiationService.createChild( new ServiceCollection([IContextKeyService, this._contextKeyService]) - ); + )); constructor( private readonly _element: HTMLElement, diff --git a/src/vs/platform/instantiation/common/instantiation.ts b/src/vs/platform/instantiation/common/instantiation.ts index 86766a4a6c6..c1ce6565271 100644 --- a/src/vs/platform/instantiation/common/instantiation.ts +++ b/src/vs/platform/instantiation/common/instantiation.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { DisposableStore } from 'vs/base/common/lifecycle'; import * as descriptors from './descriptors'; import { ServiceCollection } from './serviceCollection'; @@ -61,8 +62,11 @@ export interface IInstantiationService { /** * Creates a child of this service which inherits all current services * and adds/overwrites the given services. + * + * NOTE that the returned child is `disposable` and should be disposed when not used + * anymore. This will also dispose all the services that this service has created. */ - createChild(services: ServiceCollection): IInstantiationService; + createChild(services: ServiceCollection, store?: DisposableStore): IInstantiationService; /** * Disposes this instantiation service. diff --git a/src/vs/platform/instantiation/common/instantiationService.ts b/src/vs/platform/instantiation/common/instantiationService.ts index fd95305865a..5815924b360 100644 --- a/src/vs/platform/instantiation/common/instantiationService.ts +++ b/src/vs/platform/instantiation/common/instantiationService.ts @@ -6,7 +6,7 @@ import { GlobalIdleValue } from 'vs/base/common/async'; import { Event } from 'vs/base/common/event'; import { illegalState } from 'vs/base/common/errors'; -import { dispose, IDisposable, isDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, dispose, IDisposable, isDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { SyncDescriptor, SyncDescriptor0 } from 'vs/platform/instantiation/common/descriptors'; import { Graph } from 'vs/platform/instantiation/common/graph'; import { GetLeadingNonServiceArgs, IInstantiationService, ServiceIdentifier, ServicesAccessor, _util } from 'vs/platform/instantiation/common/instantiation'; @@ -70,7 +70,7 @@ export class InstantiationService implements IInstantiationService { } } - createChild(services: ServiceCollection): IInstantiationService { + createChild(services: ServiceCollection, store?: DisposableStore): IInstantiationService { this._throwIfDisposed(); const result = new class extends InstantiationService { @@ -80,6 +80,8 @@ export class InstantiationService implements IInstantiationService { } }(services, this._strict, this, this._enableTracing); this._children.add(result); + + store?.add(result); return result; } diff --git a/src/vs/workbench/browser/parts/compositePart.ts b/src/vs/workbench/browser/parts/compositePart.ts index 3ea632e64d5..19c56d9b509 100644 --- a/src/vs/workbench/browser/parts/compositePart.ts +++ b/src/vs/workbench/browser/parts/compositePart.ts @@ -199,6 +199,7 @@ export abstract class CompositePart extends Part { // Register to title area update events from the composite disposable.add(composite.onTitleAreaUpdate(() => this.onTitleAreaUpdate(composite.getId()), this)); + disposable.add(compositeInstantiationService); return composite; } diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index cdd2f1f8bee..0024b27a413 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -411,7 +411,7 @@ export class ChatWidget extends Disposable implements IChatWidget { } private createList(listContainer: HTMLElement, options: IChatListItemRendererOptions): void { - const scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.contextKeyService])); + const scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.contextKeyService]))); const delegate = scopedInstantiationService.createInstance(ChatListDelegate, this.viewOptions.defaultElementHeight ?? 200); const rendererDelegate: IChatRendererDelegate = { getListLength: () => this.tree.getNode(null).visibleChildrenCount, diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.ts index d5ac64ea3f1..8a64d210ece 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.ts @@ -56,7 +56,8 @@ export class InlineChatContentWidget implements IContentWidget { new ServiceCollection([ IContextKeyService, this._store.add(contextKeyService.createScoped(this._domNode)) - ]) + ]), + this._store ); this._widget = scopedInstaService.createInstance( diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index aa8c32c6391..96bf08b444a 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -158,7 +158,8 @@ export class InlineChatWidget { new ServiceCollection([ IContextKeyService, this.scopedContextKeyService - ]) + ]), + this._store ); this._chatWidget = scopedInstaService.createInstance( From 743fdd95876261bc103fec0966749a89c4a56bf7 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 16 May 2024 17:04:59 +0200 Subject: [PATCH 233/357] focus owning editor when clicking into zone without having editor widget focus (#212878) fixes https://github.com/microsoft/vscode-copilot/issues/5410 --- .../contrib/inlineChat/browser/inlineChatZoneWidget.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts index bd3840d06c4..d37ea30b024 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Dimension } from 'vs/base/browser/dom'; +import { addDisposableListener, Dimension } from 'vs/base/browser/dom'; import * as aria from 'vs/base/browser/ui/aria/aria'; import { toDisposable } from 'vs/base/common/lifecycle'; import { assertType } from 'vs/base/common/types'; @@ -81,6 +81,13 @@ export class InlineChatZoneWidget extends ZoneWidget { this._disposables.add(this.widget); this.create(); + this._disposables.add(addDisposableListener(this.domNode, 'click', e => { + if (!this.editor.hasWidgetFocus() && !this.widget.hasFocus()) { + this.editor.focus(); + } + }, true)); + + // todo@jrieken listen ONLY when showing const updateCursorIsAboveContextKey = () => { if (!this.position || !this.editor.hasModel()) { From 98eec201fb7fbc20f3a365860a3f85b1c7ff87c1 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 16 May 2024 17:12:34 +0200 Subject: [PATCH 234/357] fix #212664 (#212873) --- src/vs/platform/userDataSync/common/extensionsSync.ts | 2 +- .../services/userDataProfile/browser/extensionsResource.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/platform/userDataSync/common/extensionsSync.ts b/src/vs/platform/userDataSync/common/extensionsSync.ts index 367a89bf201..53561e249db 100644 --- a/src/vs/platform/userDataSync/common/extensionsSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsSync.ts @@ -570,7 +570,7 @@ export class LocalExtensionsProvider { return this.userDataProfileStorageService.withProfileScopedStorageService(profile, async storageService => { const disposables = new DisposableStore(); - const instantiationService = this.instantiationService.createChild(new ServiceCollection([IStorageService, storageService])); + const instantiationService = disposables.add(this.instantiationService.createChild(new ServiceCollection([IStorageService, storageService]))); const extensionEnablementService = disposables.add(instantiationService.createInstance(GlobalExtensionEnablementService)); const extensionStorageService = disposables.add(instantiationService.createInstance(ExtensionStorageService)); try { diff --git a/src/vs/workbench/services/userDataProfile/browser/extensionsResource.ts b/src/vs/workbench/services/userDataProfile/browser/extensionsResource.ts index 1fd2767bf04..06c54a5729c 100644 --- a/src/vs/workbench/services/userDataProfile/browser/extensionsResource.ts +++ b/src/vs/workbench/services/userDataProfile/browser/extensionsResource.ts @@ -239,7 +239,7 @@ export class ExtensionsResource implements IProfileResource { return this.userDataProfileStorageService.withProfileScopedStorageService(profile, async storageService => { const disposables = new DisposableStore(); - const instantiationService = this.instantiationService.createChild(new ServiceCollection([IStorageService, storageService])); + const instantiationService = disposables.add(this.instantiationService.createChild(new ServiceCollection([IStorageService, storageService]))); const extensionEnablementService = disposables.add(instantiationService.createInstance(GlobalExtensionEnablementService)); try { return await fn(extensionEnablementService); From 7f585679fd8df6629c9fe78cdbeedddb35fb5631 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 16 May 2024 17:17:28 +0200 Subject: [PATCH 235/357] fix #210837 (#212901) --- .../abstractExtensionManagementService.ts | 31 +- .../common/extensionManagement.ts | 1 + .../common/extensionManagementUtil.ts | 2 +- .../node/extensionManagementService.ts | 396 +++++++++--------- .../test/node/extensionDownloader.test.ts | 159 +++++++ .../node/installGalleryExtensionTask.test.ts | 251 ----------- 6 files changed, 377 insertions(+), 463 deletions(-) create mode 100644 src/vs/platform/extensionManagement/test/node/extensionDownloader.test.ts delete mode 100644 src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts diff --git a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts index f797cd72e80..2ac86759fb9 100644 --- a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts +++ b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts @@ -37,7 +37,6 @@ export interface IInstallExtensionTask { readonly identifier: IExtensionIdentifier; readonly source: IGalleryExtension | URI; readonly operation: InstallOperation; - readonly profileLocation: URI; readonly options: InstallExtensionTaskOptions; readonly verificationStatus?: ExtensionVerificationStatus; run(): Promise; @@ -206,7 +205,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl const key = `${getGalleryExtensionId(manifest.publisher, manifest.name)}-${options.profileLocation.toString()}`; installingExtensionsMap.set(key, { task: installExtensionTask, root }); this._onInstallExtension.fire({ identifier: installExtensionTask.identifier, source: extension, profileLocation: options.profileLocation }); - this.logService.info('Installing extension:', installExtensionTask.identifier.id); + this.logService.info('Installing extension:', installExtensionTask.identifier.id, options.profileLocation.toString()); // only cache gallery extensions tasks if (!URI.isUri(extension)) { this.installingExtensions.set(getInstallExtensionTaskKey(extension, options.profileLocation), { task: installExtensionTask, waitingTasks: [] }); @@ -227,7 +226,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl const existingInstallExtensionTask = !URI.isUri(extension) ? this.installingExtensions.get(getInstallExtensionTaskKey(extension, installExtensionTaskOptions.profileLocation)) : undefined; if (existingInstallExtensionTask) { - this.logService.info('Extension is already requested to install', existingInstallExtensionTask.task.identifier.id); + this.logService.info('Extension is already requested to install', existingInstallExtensionTask.task.identifier.id, installExtensionTaskOptions.profileLocation.toString()); alreadyRequestedInstallations.push(existingInstallExtensionTask.task.waitUntilTaskIsFinished()); } else { createInstallExtensionTask(manifest, extension, installExtensionTaskOptions, undefined); @@ -251,14 +250,14 @@ export abstract class AbstractExtensionManagementService extends Disposable impl if (existingInstallingExtension) { if (this.canWaitForTask(task, existingInstallingExtension.task)) { const identifier = existingInstallingExtension.task.identifier; - this.logService.info('Waiting for already requested installing extension', identifier.id, task.identifier.id); + this.logService.info('Waiting for already requested installing extension', identifier.id, task.identifier.id, options.profileLocation.toString()); existingInstallingExtension.waitingTasks.push(task); // add promise that waits until the extension is completely installed, ie., onDidInstallExtensions event is triggered for this extension alreadyRequestedInstallations.push( Event.toPromise( Event.filter(this.onDidInstallExtensions, results => results.some(result => areSameExtensions(result.identifier, identifier))) ).then(results => { - this.logService.info('Finished waiting for already requested installing extension', identifier.id, task.identifier.id); + this.logService.info('Finished waiting for already requested installing extension', identifier.id, task.identifier.id, options.profileLocation.toString()); const result = results.find(result => areSameExtensions(result.identifier, identifier)); if (!result?.local) { // Extension failed to install @@ -304,8 +303,8 @@ export abstract class AbstractExtensionManagementService extends Disposable impl source: task.options.context?.[EXTENSION_INSTALL_SOURCE_CONTEXT] }); } - installExtensionResultsMap.set(key, { error, identifier: task.identifier, operation: task.operation, source: task.source, context: task.options.context, profileLocation: task.profileLocation, applicationScoped: task.options.isApplicationScoped }); - this.logService.error('Error while installing the extension', task.identifier.id, getErrorMessage(error)); + installExtensionResultsMap.set(key, { error, identifier: task.identifier, operation: task.operation, source: task.source, context: task.options.context, profileLocation: task.options.profileLocation, applicationScoped: task.options.isApplicationScoped }); + this.logService.error('Error while installing the extension', task.identifier.id, getErrorMessage(error), task.options.profileLocation.toString()); throw error; } if (!URI.isUri(task.source)) { @@ -325,7 +324,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl } catch (error) { /* ignore */ } } } - installExtensionResultsMap.set(key, { local, identifier: task.identifier, operation: task.operation, source: task.source, context: task.options.context, profileLocation: task.profileLocation, applicationScoped: local.isApplicationScoped }); + installExtensionResultsMap.set(key, { local, identifier: task.identifier, operation: task.operation, source: task.source, context: task.options.context, profileLocation: task.options.profileLocation, applicationScoped: local.isApplicationScoped }); })); if (alreadyRequestedInstallations.length) { @@ -353,7 +352,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl } return allDepsOrPacks; }; - const getErrorResult = (task: IInstallExtensionTask) => ({ identifier: task.identifier, operation: InstallOperation.Install, source: task.source, context: task.options.context, profileLocation: task.profileLocation, error }); + const getErrorResult = (task: IInstallExtensionTask) => ({ identifier: task.identifier, operation: InstallOperation.Install, source: task.source, context: task.options.context, profileLocation: task.options.profileLocation, error }); const rollbackTasks: IUninstallExtensionTask[] = []; for (const [key, { task, root }] of installingExtensionsMap) { @@ -363,8 +362,8 @@ export abstract class AbstractExtensionManagementService extends Disposable impl installExtensionResultsMap.set(key, getErrorResult(task)); } // If the extension is installed by a root task and the root task is failed, then uninstall the extension - else if (result.local && root && !installExtensionResultsMap.get(`${root.identifier.id.toLowerCase()}-${task.profileLocation.toString()}`)?.local) { - rollbackTasks.push(this.createUninstallExtensionTask(result.local, { versionOnly: true, profileLocation: task.profileLocation })); + else if (result.local && root && !installExtensionResultsMap.get(`${root.identifier.id.toLowerCase()}-${task.options.profileLocation.toString()}`)?.local) { + rollbackTasks.push(this.createUninstallExtensionTask(result.local, { versionOnly: true, profileLocation: task.options.profileLocation })); installExtensionResultsMap.set(key, getErrorResult(task)); } } @@ -376,9 +375,9 @@ export abstract class AbstractExtensionManagementService extends Disposable impl if (task.options.donotIncludePackAndDependencies) { continue; } - const depsOrPacks = getAllDepsAndPacks(result.local, task.profileLocation, [result.local.identifier.id.toLowerCase()]).slice(1); - if (depsOrPacks.some(depOrPack => installingExtensionsMap.has(`${depOrPack.toLowerCase()}-${task.profileLocation.toString()}`) && !installExtensionResultsMap.get(`${depOrPack.toLowerCase()}-${task.profileLocation.toString()}`)?.local)) { - rollbackTasks.push(this.createUninstallExtensionTask(result.local, { versionOnly: true, profileLocation: task.profileLocation })); + const depsOrPacks = getAllDepsAndPacks(result.local, task.options.profileLocation, [result.local.identifier.id.toLowerCase()]).slice(1); + if (depsOrPacks.some(depOrPack => installingExtensionsMap.has(`${depOrPack.toLowerCase()}-${task.options.profileLocation.toString()}`) && !installExtensionResultsMap.get(`${depOrPack.toLowerCase()}-${task.options.profileLocation.toString()}`)?.local)) { + rollbackTasks.push(this.createUninstallExtensionTask(result.local, { versionOnly: true, profileLocation: task.options.profileLocation })); installExtensionResultsMap.set(key, getErrorResult(task)); } } @@ -399,14 +398,14 @@ export abstract class AbstractExtensionManagementService extends Disposable impl // Finally, remove all the tasks from the cache for (const { task } of installingExtensionsMap.values()) { if (task.source && !URI.isUri(task.source)) { - this.installingExtensions.delete(getInstallExtensionTaskKey(task.source, task.profileLocation)); + this.installingExtensions.delete(getInstallExtensionTaskKey(task.source, task.options.profileLocation)); } } if (installExtensionResultsMap.size) { const results = [...installExtensionResultsMap.values()]; for (const result of results) { if (result.local) { - this.logService.info(`Extension installed successfully:`, result.identifier.id); + this.logService.info(`Extension installed successfully:`, result.identifier.id, result.profileLocation.toString()); } } this._onDidInstallExtensions.fire(results); diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 902d2d9bee9..183f2582871 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -448,6 +448,7 @@ export const enum ExtensionManagementErrorCode { Rename = 'Rename', IntializeDefaultProfile = 'IntializeDefaultProfile', AddToProfile = 'AddToProfile', + InstalledExtensionNotFound = 'InstalledExtensionNotFound', PostInstall = 'PostInstall', CorruptZip = 'CorruptZip', IncompleteZip = 'IncompleteZip', diff --git a/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts b/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts index 935f836438f..301c7b18e70 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts @@ -42,7 +42,7 @@ export class ExtensionKey { readonly id: string; constructor( - identifier: IExtensionIdentifier, + readonly identifier: IExtensionIdentifier, readonly version: string, readonly targetPlatform: TargetPlatform = TargetPlatform.UNDEFINED, ) { diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 835f0ebc632..38757db3a67 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -8,7 +8,7 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IStringDictionary } from 'vs/base/common/collections'; import { toErrorMessage } from 'vs/base/common/errorMessage'; -import { getErrorMessage } from 'vs/base/common/errors'; +import { CancellationError, getErrorMessage } from 'vs/base/common/errors'; import { Emitter } from 'vs/base/common/event'; import { hash } from 'vs/base/common/hash'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -17,7 +17,7 @@ import { Schemas } from 'vs/base/common/network'; import * as path from 'vs/base/common/path'; import { joinPath } from 'vs/base/common/resources'; import * as semver from 'vs/base/common/semver/semver'; -import { isBoolean, isUndefined } from 'vs/base/common/types'; +import { isBoolean } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import * as pfs from 'vs/base/node/pfs'; @@ -30,7 +30,7 @@ import { ExtensionManagementError, ExtensionManagementErrorCode, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService, IGalleryExtension, ILocalExtension, InstallOperation, Metadata, InstallOptions, IProductVersion, - EXTENSION_INSTALL_CLIENT_TARGET_PLATFORM_CONTEXT + EXTENSION_INSTALL_CLIENT_TARGET_PLATFORM_CONTEXT, } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions, computeTargetPlatform, ExtensionKey, getGalleryExtensionId, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IExtensionsProfileScannerService, IScannedProfileExtension } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService'; @@ -50,12 +50,6 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; -interface InstallableExtension { - zipPath: string; - key: ExtensionKey; - metadata: Metadata; -} - export const INativeServerExtensionManagementService = refineServiceDecorator(IExtensionManagementService); export interface INativeServerExtensionManagementService extends IExtensionManagementService { readonly _serviceBrand: undefined; @@ -64,6 +58,8 @@ export interface INativeServerExtensionManagementService extends IExtensionManag markAsUninstalled(...extensions: IExtension[]): Promise; } +type ExtractExtensionResult = { readonly local: ILocalExtension; readonly verificationStatus?: ExtensionVerificationStatus }; + const DELETED_FOLDER_POSTFIX = '.vsctmp'; export class ExtensionManagementService extends AbstractExtensionManagementService implements INativeServerExtensionManagementService { @@ -72,7 +68,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi private readonly manifestCache: ExtensionsManifestCache; private readonly extensionsDownloader: ExtensionsDownloader; - private readonly installGalleryExtensionsTasks = new Map(); + private readonly extractingGalleryExtensions = new Map>(); constructor( @IExtensionGalleryService galleryService: IExtensionGalleryService, @@ -82,7 +78,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi @IExtensionsScannerService private readonly extensionsScannerService: IExtensionsScannerService, @IExtensionsProfileScannerService private readonly extensionsProfileScannerService: IExtensionsProfileScannerService, @IDownloadService private downloadService: IDownloadService, - @IInstantiationService instantiationService: IInstantiationService, + @IInstantiationService private readonly instantiationService: IInstantiationService, @IFileService private readonly fileService: IFileService, @IProductService productService: IProductService, @IUriIdentityService uriIdentityService: IUriIdentityService, @@ -282,21 +278,84 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi } protected createInstallExtensionTask(manifest: IExtensionManifest, extension: URI | IGalleryExtension, options: InstallExtensionTaskOptions): IInstallExtensionTask { - if (URI.isUri(extension)) { - return new InstallVSIXTask(manifest, extension, options, this.galleryService, this.extensionsScanner, this.uriIdentityService, this.userDataProfilesService, this.extensionsScannerService, this.extensionsProfileScannerService, this.logService); - } - - const key = ExtensionKey.create(extension).toString(); - let installExtensionTask = this.installGalleryExtensionsTasks.get(key); - if (!installExtensionTask) { - this.installGalleryExtensionsTasks.set(key, installExtensionTask = new InstallGalleryExtensionTask(manifest, extension, options, this.extensionsDownloader, this.extensionsScanner, this.uriIdentityService, this.userDataProfilesService, this.extensionsScannerService, this.extensionsProfileScannerService, this.logService)); - installExtensionTask.waitUntilTaskIsFinished().finally(() => this.installGalleryExtensionsTasks.delete(key)); - } - return installExtensionTask; + const extensionKey = extension instanceof URI ? new ExtensionKey({ id: getGalleryExtensionId(manifest.publisher, manifest.name) }, manifest.version) : ExtensionKey.create(extension); + return this.instantiationService.createInstance(InstallExtensionInProfileTask, extensionKey, manifest, extension, options, (operation, token) => { + if (extension instanceof URI) { + return this.extractVSIX(extensionKey, extension, options, token); + } + let promise = this.extractingGalleryExtensions.get(extensionKey.toString()); + if (!promise) { + this.extractingGalleryExtensions.set(extensionKey.toString(), promise = this.downloadAndExtractGalleryExtension(extensionKey, extension, operation, options, token)); + promise.finally(() => this.extractingGalleryExtensions.delete(extensionKey.toString())); + } + return promise; + }, this.extensionsScanner); } protected createUninstallExtensionTask(extension: ILocalExtension, options: UninstallExtensionTaskOptions): IUninstallExtensionTask { - return new UninstallExtensionTask(extension, options.profileLocation, this.extensionsProfileScannerService); + return new UninstallExtensionInProfileTask(extension, options.profileLocation, this.extensionsProfileScannerService); + } + + private async downloadAndExtractGalleryExtension(extensionKey: ExtensionKey, gallery: IGalleryExtension, operation: InstallOperation, options: InstallExtensionTaskOptions, token: CancellationToken): Promise { + const { verificationStatus, location } = await this.extensionsDownloader.download(gallery, operation, !options.donotVerifySignature, options.context?.[EXTENSION_INSTALL_CLIENT_TARGET_PLATFORM_CONTEXT]); + try { + + if (token.isCancellationRequested) { + throw new CancellationError(); + } + + // validate manifest + await getManifest(location.fsPath); + + const local = await this.extensionsScanner.extractUserExtension( + extensionKey, + location.fsPath, + { + id: gallery.identifier.uuid, + publisherId: gallery.publisherId, + publisherDisplayName: gallery.publisherDisplayName, + targetPlatform: gallery.properties.targetPlatform, + isApplicationScoped: options.isApplicationScoped, + isMachineScoped: options.isMachineScoped, + isBuiltin: options.isBuiltin, + isPreReleaseVersion: gallery.properties.isPreReleaseVersion, + hasPreReleaseVersion: gallery.properties.isPreReleaseVersion, + installedTimestamp: Date.now(), + pinned: options.installGivenVersion ? true : !!options.pinned, + preRelease: isBoolean(options.preRelease) + ? options.preRelease + : options.installPreReleaseVersion || gallery.properties.isPreReleaseVersion, + source: 'gallery', + }, + false, + token); + return { local, verificationStatus }; + } catch (error) { + try { + await this.extensionsDownloader.delete(location); + } catch (e) { + /* Ignore */ + this.logService.warn(`Error while deleting the downloaded file`, location.toString(), getErrorMessage(e)); + } + throw toExtensionManagementError(error); + } + } + + private async extractVSIX(extensionKey: ExtensionKey, location: URI, options: InstallExtensionTaskOptions, token: CancellationToken): Promise { + const local = await this.extensionsScanner.extractUserExtension( + extensionKey, + path.resolve(location.fsPath), + { + isApplicationScoped: options.isApplicationScoped, + isMachineScoped: options.isMachineScoped, + isBuiltin: options.isBuiltin, + installedTimestamp: Date.now(), + pinned: options.installGivenVersion ? true : !!options.pinned, + source: 'vsix', + }, + true, + token); + return { local }; } private async collectFiles(extension: ILocalExtension): Promise { @@ -512,6 +571,10 @@ export class ExtensionsScanner extends Disposable { } } else { try { + if (token.isCancellationRequested) { + throw new CancellationError(); + } + // Extract try { this.logService.trace(`Started extracting the extension from ${zipPath} to ${extensionLocation.fsPath}`); @@ -527,6 +590,10 @@ export class ExtensionsScanner extends Disposable { throw toExtensionManagementError(error, ExtensionManagementErrorCode.UpdateMetadata); } + if (token.isCancellationRequested) { + throw new CancellationError(); + } + // Rename try { this.logService.trace(`Started renaming the extension from ${tempLocation.fsPath} to ${extensionLocation.fsPath}`); @@ -816,63 +883,142 @@ export class ExtensionsScanner extends Disposable { } -abstract class InstallExtensionTask extends AbstractExtensionTask implements IInstallExtensionTask { +class InstallExtensionInProfileTask extends AbstractExtensionTask implements IInstallExtensionTask { - private _profileLocation = this.options.profileLocation; - get profileLocation() { return this._profileLocation; } + private _operation = InstallOperation.Install; + get operation() { return this.options.operation ?? this._operation; } - protected _verificationStatus: ExtensionVerificationStatus = false; + private _verificationStatus: ExtensionVerificationStatus | undefined; get verificationStatus() { return this._verificationStatus; } - protected _operation = InstallOperation.Install; - get operation() { return isUndefined(this.options.operation) ? this._operation : this.options.operation; } + readonly identifier: IExtensionIdentifier; constructor( + private readonly extensionKey: ExtensionKey, readonly manifest: IExtensionManifest, - readonly identifier: IExtensionIdentifier, - readonly source: URI | IGalleryExtension, + readonly source: IGalleryExtension | URI, readonly options: InstallExtensionTaskOptions, - protected readonly extensionsScanner: ExtensionsScanner, - protected readonly uriIdentityService: IUriIdentityService, - protected readonly userDataProfilesService: IUserDataProfilesService, - protected readonly extensionsScannerService: IExtensionsScannerService, - protected readonly extensionsProfileScannerService: IExtensionsProfileScannerService, - protected readonly logService: ILogService, + private readonly extractExtensionFn: (operation: InstallOperation, token: CancellationToken) => Promise, + private readonly extensionsScanner: ExtensionsScanner, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, + @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, + @IExtensionsScannerService private readonly extensionsScannerService: IExtensionsScannerService, + @IExtensionsProfileScannerService private readonly extensionsProfileScannerService: IExtensionsProfileScannerService, + @ILogService private readonly logService: ILogService, ) { super(); + this.identifier = this.extensionKey.identifier; } - protected override async doRun(token: CancellationToken): Promise { - const [local, metadata] = await this.install(token); - this._profileLocation = local.isBuiltin || local.isApplicationScoped ? this.userDataProfilesService.defaultProfile.extensionsResource : this.options.profileLocation; - if (this.uriIdentityService.extUri.isEqual(this.userDataProfilesService.defaultProfile.extensionsResource, this._profileLocation)) { + private async getExistingExtension(): Promise { + const installed = await this.extensionsScanner.scanExtensions(null, this.options.profileLocation, this.options.productVersion); + return installed.find(i => areSameExtensions(i.identifier, this.identifier)); + } + + protected async doRun(token: CancellationToken): Promise { + const existingExtension = await this.getExistingExtension(); + if (existingExtension) { + this._operation = InstallOperation.Update; + } + + const metadata: Metadata = { + isApplicationScoped: this.options.isApplicationScoped || existingExtension?.isApplicationScoped, + isMachineScoped: this.options.isMachineScoped || existingExtension?.isMachineScoped, + isBuiltin: this.options.isBuiltin || existingExtension?.isBuiltin, + isSystem: existingExtension?.type === ExtensionType.System ? true : undefined, + installedTimestamp: Date.now(), + pinned: this.options.installGivenVersion ? true : (this.options.pinned ?? existingExtension?.pinned), + source: this.source instanceof URI ? 'vsix' : 'gallery', + }; + + // VSIX + if (this.source instanceof URI) { + if (existingExtension) { + if (this.extensionKey.equals(new ExtensionKey(existingExtension.identifier, existingExtension.manifest.version))) { + try { + await this.extensionsScanner.removeExtension(existingExtension, 'existing'); + } catch (e) { + throw new Error(nls.localize('restartCode', "Please restart VS Code before reinstalling {0}.", this.manifest.displayName || this.manifest.name)); + } + } + } else { + // Remove the extension with same version if it is already uninstalled. + // Installing a VSIX extension shall replace the existing extension always. + const existingWithSameVersion = await this.unsetIfUninstalled(this.extensionKey); + if (existingWithSameVersion) { + try { + await this.extensionsScanner.removeExtension(existingWithSameVersion, 'existing'); + } catch (e) { + throw new Error(nls.localize('restartCode', "Please restart VS Code before reinstalling {0}.", this.manifest.displayName || this.manifest.name)); + } + } + } + + } + + // Gallery + else { + metadata.id = this.source.identifier.uuid; + metadata.publisherId = this.source.publisherId; + metadata.publisherDisplayName = this.source.publisherDisplayName; + metadata.targetPlatform = this.source.properties.targetPlatform; + metadata.updated = !!existingExtension; + metadata.isPreReleaseVersion = this.source.properties.isPreReleaseVersion; + metadata.hasPreReleaseVersion = existingExtension?.hasPreReleaseVersion || this.source.properties.isPreReleaseVersion; + metadata.preRelease = isBoolean(this.options.preRelease) + ? this.options.preRelease + : this.options.installPreReleaseVersion || this.source.properties.isPreReleaseVersion || existingExtension?.preRelease; + + if (existingExtension && existingExtension.type !== ExtensionType.System && existingExtension.manifest.version === this.source.version) { + return this.extensionsScanner.updateMetadata(existingExtension, metadata); + } + + // Unset if the extension is uninstalled and return the unset extension. + const local = await this.unsetIfUninstalled(this.extensionKey); + if (local) { + return local; + } + } + + if (token.isCancellationRequested) { + throw toExtensionManagementError(new CancellationError()); + } + + const { local, verificationStatus } = await this.extractExtensionFn(this.operation, token); + this._verificationStatus = verificationStatus; + + if (this.uriIdentityService.extUri.isEqual(this.userDataProfilesService.defaultProfile.extensionsResource, this.options.profileLocation)) { try { await this.extensionsScannerService.initializeDefaultProfileExtensions(); } catch (error) { throw toExtensionManagementError(error, ExtensionManagementErrorCode.IntializeDefaultProfile); } } + + if (token.isCancellationRequested) { + throw toExtensionManagementError(new CancellationError()); + } + try { - await this.extensionsProfileScannerService.addExtensionsToProfile([[local, metadata]], this._profileLocation, !local.isValid); + await this.extensionsProfileScannerService.addExtensionsToProfile([[local, metadata]], this.options.profileLocation, !local.isValid); } catch (error) { throw toExtensionManagementError(error, ExtensionManagementErrorCode.AddToProfile); } - return local; - } - protected async extractExtension({ zipPath, key, metadata }: InstallableExtension, removeIfExists: boolean, token: CancellationToken): Promise { - let local = await this.unsetIfUninstalled(key); - if (local) { - local = await this.extensionsScanner.updateMetadata(local, metadata); - } else { - this.logService.trace('Extracting extension...', key.id); - local = await this.extensionsScanner.extractUserExtension(key, zipPath, metadata, removeIfExists, token); - this.logService.info('Extracting extension completed.', key.id); + const result = await this.getExistingExtension(); + if (!result) { + throw new ExtensionManagementError('Cannot find the installed extension', ExtensionManagementErrorCode.InstalledExtensionNotFound); } - return local; + + if (this.source instanceof URI) { + this.updateMetadata(local, token); + } + + return result; } - protected async unsetIfUninstalled(extensionKey: ExtensionKey): Promise { + private async unsetIfUninstalled(extensionKey: ExtensionKey): Promise { const uninstalled = await this.extensionsScanner.getUninstalledExtensions(); if (!uninstalled[extensionKey.toString()]) { return undefined; @@ -887,145 +1033,6 @@ abstract class InstallExtensionTask extends AbstractExtensionTask ExtensionKey.create(i).equals(extensionKey)); } - protected abstract install(token: CancellationToken): Promise<[ILocalExtension, Metadata]>; - -} - -export class InstallGalleryExtensionTask extends InstallExtensionTask { - - constructor( - manifest: IExtensionManifest, - private readonly gallery: IGalleryExtension, - options: InstallExtensionTaskOptions, - private readonly extensionsDownloader: ExtensionsDownloader, - extensionsScanner: ExtensionsScanner, - uriIdentityService: IUriIdentityService, - userDataProfilesService: IUserDataProfilesService, - extensionsScannerService: IExtensionsScannerService, - extensionsProfileScannerService: IExtensionsProfileScannerService, - logService: ILogService, - ) { - super(manifest, gallery.identifier, gallery, options, extensionsScanner, uriIdentityService, userDataProfilesService, extensionsScannerService, extensionsProfileScannerService, logService); - } - - protected async install(token: CancellationToken): Promise<[ILocalExtension, Metadata]> { - const installed = await this.extensionsScanner.scanExtensions(null, this.options.profileLocation, this.options.productVersion); - const existingExtension = installed.find(i => areSameExtensions(i.identifier, this.gallery.identifier)); - if (existingExtension) { - this._operation = InstallOperation.Update; - } - - const metadata: Metadata = { - id: this.gallery.identifier.uuid, - publisherId: this.gallery.publisherId, - publisherDisplayName: this.gallery.publisherDisplayName, - targetPlatform: this.gallery.properties.targetPlatform, - isApplicationScoped: this.options.isApplicationScoped || existingExtension?.isApplicationScoped, - isMachineScoped: this.options.isMachineScoped || existingExtension?.isMachineScoped, - isBuiltin: this.options.isBuiltin || existingExtension?.isBuiltin, - isSystem: existingExtension?.type === ExtensionType.System ? true : undefined, - updated: !!existingExtension, - isPreReleaseVersion: this.gallery.properties.isPreReleaseVersion, - hasPreReleaseVersion: existingExtension?.hasPreReleaseVersion || this.gallery.properties.isPreReleaseVersion, - installedTimestamp: Date.now(), - pinned: this.options.installGivenVersion ? true : (this.options.pinned ?? existingExtension?.pinned), - preRelease: isBoolean(this.options.preRelease) - ? this.options.preRelease - : this.options.installPreReleaseVersion || this.gallery.properties.isPreReleaseVersion || existingExtension?.preRelease, - source: 'gallery', - }; - - if (existingExtension && existingExtension.type !== ExtensionType.System && existingExtension.manifest.version === this.gallery.version) { - const local = await this.extensionsScanner.updateMetadata(existingExtension, metadata); - return [local, metadata]; - } - - const { verificationStatus, location } = await this.extensionsDownloader.download(this.gallery, this._operation, !this.options.donotVerifySignature, this.options.context?.[EXTENSION_INSTALL_CLIENT_TARGET_PLATFORM_CONTEXT]); - try { - this._verificationStatus = verificationStatus; - await this.validateManifest(location.fsPath); - const local = await this.extractExtension({ zipPath: location.fsPath, key: ExtensionKey.create(this.gallery), metadata }, false, token); - return [local, metadata]; - } catch (error) { - try { - await this.extensionsDownloader.delete(location); - } catch (e) { - /* Ignore */ - this.logService.warn(`Error while deleting the downloaded file`, location.toString(), getErrorMessage(e)); - } - throw error; - } - } - - protected async validateManifest(zipPath: string): Promise { - await getManifest(zipPath); - } -} - -class InstallVSIXTask extends InstallExtensionTask { - - constructor( - manifest: IExtensionManifest, - private readonly location: URI, - options: InstallExtensionTaskOptions, - private readonly galleryService: IExtensionGalleryService, - extensionsScanner: ExtensionsScanner, - uriIdentityService: IUriIdentityService, - userDataProfilesService: IUserDataProfilesService, - extensionsScannerService: IExtensionsScannerService, - extensionsProfileScannerService: IExtensionsProfileScannerService, - logService: ILogService, - ) { - super(manifest, { id: getGalleryExtensionId(manifest.publisher, manifest.name) }, location, options, extensionsScanner, uriIdentityService, userDataProfilesService, extensionsScannerService, extensionsProfileScannerService, logService); - } - - protected override async doRun(token: CancellationToken): Promise { - const local = await super.doRun(token); - this.updateMetadata(local, token); - return local; - } - - protected async install(token: CancellationToken): Promise<[ILocalExtension, Metadata]> { - const extensionKey = new ExtensionKey(this.identifier, this.manifest.version); - const installedExtensions = await this.extensionsScanner.scanExtensions(ExtensionType.User, this.options.profileLocation, this.options.productVersion); - const existing = installedExtensions.find(i => areSameExtensions(this.identifier, i.identifier)); - const metadata: Metadata = { - isApplicationScoped: this.options.isApplicationScoped || existing?.isApplicationScoped, - isMachineScoped: this.options.isMachineScoped || existing?.isMachineScoped, - isBuiltin: this.options.isBuiltin || existing?.isBuiltin, - installedTimestamp: Date.now(), - pinned: this.options.installGivenVersion ? true : (this.options.pinned ?? existing?.pinned), - source: 'vsix', - }; - - if (existing) { - this._operation = InstallOperation.Update; - if (extensionKey.equals(new ExtensionKey(existing.identifier, existing.manifest.version))) { - try { - await this.extensionsScanner.removeExtension(existing, 'existing'); - } catch (e) { - throw new Error(nls.localize('restartCode', "Please restart VS Code before reinstalling {0}.", this.manifest.displayName || this.manifest.name)); - } - } else if (!this.options.profileLocation && semver.gt(existing.manifest.version, this.manifest.version)) { - await this.extensionsScanner.setUninstalled(existing); - } - } else { - // Remove the extension with same version if it is already uninstalled. - // Installing a VSIX extension shall replace the existing extension always. - const existing = await this.unsetIfUninstalled(extensionKey); - if (existing) { - try { - await this.extensionsScanner.removeExtension(existing, 'existing'); - } catch (e) { - throw new Error(nls.localize('restartCode', "Please restart VS Code before reinstalling {0}.", this.manifest.displayName || this.manifest.name)); - } - } - } - - const local = await this.extractExtension({ zipPath: path.resolve(this.location.fsPath), key: extensionKey, metadata }, true, token); - return [local, metadata]; - } - private async updateMetadata(extension: ILocalExtension, token: CancellationToken): Promise { try { let [galleryExtension] = await this.galleryService.getExtensions([{ id: extension.identifier.id, version: extension.manifest.version }], token); @@ -1049,7 +1056,7 @@ class InstallVSIXTask extends InstallExtensionTask { } } -class UninstallExtensionTask extends AbstractExtensionTask implements IUninstallExtensionTask { +class UninstallExtensionInProfileTask extends AbstractExtensionTask implements IUninstallExtensionTask { constructor( readonly extension: ILocalExtension, @@ -1064,4 +1071,3 @@ class UninstallExtensionTask extends AbstractExtensionTask implements IUni } } - diff --git a/src/vs/platform/extensionManagement/test/node/extensionDownloader.test.ts b/src/vs/platform/extensionManagement/test/node/extensionDownloader.test.ts new file mode 100644 index 00000000000..e803de72c79 --- /dev/null +++ b/src/vs/platform/extensionManagement/test/node/extensionDownloader.test.ts @@ -0,0 +1,159 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { platform } from 'vs/base/common/platform'; +import { arch } from 'vs/base/common/process'; +import { joinPath } from 'vs/base/common/resources'; +import { isBoolean } from 'vs/base/common/types'; +import { URI } from 'vs/base/common/uri'; +import { generateUuid } from 'vs/base/common/uuid'; +import { mock } from 'vs/base/test/common/mock'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; +import { getTargetPlatform, IExtensionGalleryService, IGalleryExtension, IGalleryExtensionAssets, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { ExtensionsDownloader } from 'vs/platform/extensionManagement/node/extensionDownloader'; +import { IExtensionSignatureVerificationService } from 'vs/platform/extensionManagement/node/extensionSignatureVerificationService'; +import { IFileService } from 'vs/platform/files/common/files'; +import { FileService } from 'vs/platform/files/common/fileService'; +import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { ILogService, NullLogService } from 'vs/platform/log/common/log'; + +const ROOT = URI.file('tests').with({ scheme: 'vscode-tests' }); + +class TestExtensionSignatureVerificationService extends mock() { + + constructor( + private readonly verificationResult: string | boolean) { + super(); + } + + override async verify(): Promise { + if (isBoolean(this.verificationResult)) { + return this.verificationResult; + } + const error = Error(this.verificationResult); + (error as any).code = this.verificationResult; + throw error; + } +} + +class TestExtensionDownloader extends ExtensionsDownloader { + protected override async validate(): Promise { } +} + +suite('ExtensionDownloader Tests', () => { + + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + let instantiationService: TestInstantiationService; + + setup(() => { + instantiationService = disposables.add(new TestInstantiationService()); + + const logService = new NullLogService(); + const fileService = disposables.add(new FileService(logService)); + const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider()); + disposables.add(fileService.registerProvider(ROOT.scheme, fileSystemProvider)); + + instantiationService.stub(ILogService, logService); + instantiationService.stub(IFileService, fileService); + instantiationService.stub(ILogService, logService); + instantiationService.stub(INativeEnvironmentService, { extensionsDownloadLocation: joinPath(ROOT, 'CachedExtensionVSIXs') }); + instantiationService.stub(IExtensionGalleryService, { + async download(extension, location, operation) { + await fileService.writeFile(location, VSBuffer.fromString('extension vsix')); + }, + async downloadSignatureArchive(extension, location) { + await fileService.writeFile(location, VSBuffer.fromString('extension signature')); + }, + }); + }); + + test('download completes successfully if verification is disabled by setting set to false', async () => { + const testObject = aTestObject({ isSignatureVerificationEnabled: false, verificationResult: 'error' }); + + const actual = await testObject.download(aGalleryExtension('a', { isSigned: true }), InstallOperation.Install, true); + + assert.strictEqual(actual.verificationStatus, false); + }); + + test('download completes successfully if verification is disabled by options', async () => { + const testObject = aTestObject({ isSignatureVerificationEnabled: true, verificationResult: 'error' }); + + const actual = await testObject.download(aGalleryExtension('a', { isSigned: true }), InstallOperation.Install, false); + + assert.strictEqual(actual.verificationStatus, false); + }); + + test('download completes successfully if verification is disabled because the module is not loaded', async () => { + const testObject = aTestObject({ isSignatureVerificationEnabled: true, verificationResult: false }); + + const actual = await testObject.download(aGalleryExtension('a', { isSigned: true }), InstallOperation.Install, true); + + assert.strictEqual(actual.verificationStatus, false); + }); + + test('download completes successfully if verification fails to execute', async () => { + const errorCode = 'ENOENT'; + const testObject = aTestObject({ isSignatureVerificationEnabled: true, verificationResult: errorCode }); + + const actual = await testObject.download(aGalleryExtension('a', { isSigned: true }), InstallOperation.Install, true); + + assert.strictEqual(actual.verificationStatus, errorCode); + }); + + test('download completes successfully if verification fails ', async () => { + const errorCode = 'IntegrityCheckFailed'; + const testObject = aTestObject({ isSignatureVerificationEnabled: true, verificationResult: errorCode }); + + const actual = await testObject.download(aGalleryExtension('a', { isSigned: true }), InstallOperation.Install, true); + + assert.strictEqual(actual.verificationStatus, errorCode); + }); + + test('download completes successfully if verification succeeds', async () => { + const testObject = aTestObject({ isSignatureVerificationEnabled: true, verificationResult: true }); + + const actual = await testObject.download(aGalleryExtension('a', { isSigned: true }), InstallOperation.Install, true); + + assert.strictEqual(actual.verificationStatus, true); + }); + + test('download completes successfully for unsigned extension', async () => { + const testObject = aTestObject({ isSignatureVerificationEnabled: true, verificationResult: true }); + + const actual = await testObject.download(aGalleryExtension('a', { isSigned: false }), InstallOperation.Install, true); + + assert.strictEqual(actual.verificationStatus, false); + }); + + test('download completes successfully for an unsigned extension even when signature verification throws error', async () => { + const testObject = aTestObject({ isSignatureVerificationEnabled: true, verificationResult: 'error' }); + + const actual = await testObject.download(aGalleryExtension('a', { isSigned: false }), InstallOperation.Install, true); + + assert.strictEqual(actual.verificationStatus, false); + }); + + function aTestObject(options: { isSignatureVerificationEnabled: boolean; verificationResult: boolean | string }): ExtensionsDownloader { + instantiationService.stub(IConfigurationService, new TestConfigurationService(isBoolean(options.isSignatureVerificationEnabled) ? { extensions: { verifySignature: options.isSignatureVerificationEnabled } } : undefined)); + instantiationService.stub(IExtensionSignatureVerificationService, new TestExtensionSignatureVerificationService(options.verificationResult)); + return disposables.add(instantiationService.createInstance(TestExtensionDownloader)); + } + + function aGalleryExtension(name: string, properties: Partial = {}, galleryExtensionProperties: any = {}, assets: Partial = {}): IGalleryExtension { + const targetPlatform = getTargetPlatform(platform, arch); + const galleryExtension = Object.create({ name, publisher: 'pub', version: '1.0.0', allTargetPlatforms: [targetPlatform], properties: {}, assets: {}, ...properties }); + galleryExtension.properties = { ...galleryExtension.properties, dependencies: [], targetPlatform, ...galleryExtensionProperties }; + galleryExtension.assets = { ...galleryExtension.assets, ...assets }; + galleryExtension.identifier = { id: getGalleryExtensionId(galleryExtension.publisher, galleryExtension.name), uuid: generateUuid() }; + return galleryExtension; + } +}); diff --git a/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts b/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts deleted file mode 100644 index 54b1f33b471..00000000000 --- a/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts +++ /dev/null @@ -1,251 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as assert from 'assert'; -import { VSBuffer } from 'vs/base/common/buffer'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { DisposableStore } from 'vs/base/common/lifecycle'; -import { platform } from 'vs/base/common/platform'; -import { arch } from 'vs/base/common/process'; -import { joinPath } from 'vs/base/common/resources'; -import { isBoolean } from 'vs/base/common/types'; -import { URI } from 'vs/base/common/uri'; -import { generateUuid } from 'vs/base/common/uuid'; -import { mock } from 'vs/base/test/common/mock'; -import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; -import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; -import { getTargetPlatform, IExtensionGalleryService, IGalleryExtension, IGalleryExtensionAssets, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { IExtensionsProfileScannerService } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService'; -import { IExtensionsScannerService } from 'vs/platform/extensionManagement/common/extensionsScannerService'; -import { ExtensionsDownloader } from 'vs/platform/extensionManagement/node/extensionDownloader'; -import { ExtensionsScanner, InstallGalleryExtensionTask } from 'vs/platform/extensionManagement/node/extensionManagementService'; -import { IExtensionSignatureVerificationService } from 'vs/platform/extensionManagement/node/extensionSignatureVerificationService'; -import { ExtensionsProfileScannerService } from 'vs/platform/extensionManagement/node/extensionsProfileScannerService'; -import { ExtensionsScannerService } from 'vs/platform/extensionManagement/node/extensionsScannerService'; -import { IFileService } from 'vs/platform/files/common/files'; -import { FileService } from 'vs/platform/files/common/fileService'; -import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; -import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; -import { ILogService, NullLogService } from 'vs/platform/log/common/log'; -import { IProductService } from 'vs/platform/product/common/productService'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; -import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; -import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; -import { IUserDataProfilesService, UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; - -const ROOT = URI.file('tests').with({ scheme: 'vscode-tests' }); - -class TestExtensionsScanner extends mock() { - override async scanExtensions(): Promise { return []; } -} - -class TestExtensionSignatureVerificationService extends mock() { - - constructor( - private readonly verificationResult: string | boolean) { - super(); - } - - override async verify(): Promise { - if (isBoolean(this.verificationResult)) { - return this.verificationResult; - } - const error = Error(this.verificationResult); - (error as any).code = this.verificationResult; - throw error; - } -} - -class TestInstallGalleryExtensionTask extends InstallGalleryExtensionTask { - - installed = false; - - constructor( - extension: IGalleryExtension, - extensionDownloader: ExtensionsDownloader, - disposables: DisposableStore, - ) { - const instantiationService = disposables.add(new TestInstantiationService()); - const logService = instantiationService.stub(ILogService, new NullLogService()); - const fileService = instantiationService.stub(IFileService, disposables.add(new FileService(logService))); - const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider()); - disposables.add(fileService.registerProvider(ROOT.scheme, fileSystemProvider)); - const systemExtensionsLocation = joinPath(ROOT, 'system'); - const userExtensionsLocation = joinPath(ROOT, 'extensions'); - instantiationService.stub(INativeEnvironmentService, { - userHome: ROOT, - userRoamingDataHome: ROOT, - builtinExtensionsPath: systemExtensionsLocation.fsPath, - extensionsPath: userExtensionsLocation.fsPath, - userDataPath: userExtensionsLocation.fsPath, - cacheHome: ROOT, - }); - instantiationService.stub(IProductService, {}); - instantiationService.stub(ITelemetryService, NullTelemetryService); - const uriIdentityService = instantiationService.stub(IUriIdentityService, disposables.add(instantiationService.createInstance(UriIdentityService))); - const userDataProfilesService = instantiationService.stub(IUserDataProfilesService, disposables.add(instantiationService.createInstance(UserDataProfilesService))); - const extensionsProfileScannerService = instantiationService.stub(IExtensionsProfileScannerService, disposables.add(instantiationService.createInstance(ExtensionsProfileScannerService))); - const extensionsScannerService = instantiationService.stub(IExtensionsScannerService, disposables.add(instantiationService.createInstance(ExtensionsScannerService))); - super( - { - name: extension.name, - publisher: extension.publisher, - version: extension.version, - engines: { vscode: '*' }, - }, - extension, - { profileLocation: userDataProfilesService.defaultProfile.extensionsResource, productVersion: { version: '' } }, - extensionDownloader, - new TestExtensionsScanner(), - uriIdentityService, - userDataProfilesService, - extensionsScannerService, - extensionsProfileScannerService, - logService - ); - } - - protected override async doRun(token: CancellationToken): Promise { - const result = await this.install(token); - return result[0]; - } - - protected override async extractExtension(): Promise { - this.installed = true; - return new class extends mock() { }; - } - - protected override async validateManifest(): Promise { } -} - -class TestExtensionDownloader extends ExtensionsDownloader { - protected override async validate(): Promise { } -} - -suite('InstallGalleryExtensionTask Tests', () => { - - const disposables = ensureNoDisposablesAreLeakedInTestSuite(); - - test('if verification is enabled by default, the task completes', async () => { - const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader({ isSignatureVerificationEnabled: true, verificationResult: true }), disposables.add(new DisposableStore())); - - await testObject.run(); - - assert.strictEqual(testObject.verificationStatus, true); - assert.strictEqual(testObject.installed, true); - }); - - test('if verification is enabled in stable, the task completes', async () => { - const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader({ isSignatureVerificationEnabled: true, verificationResult: true, quality: 'stable' }), disposables.add(new DisposableStore())); - - await testObject.run(); - - assert.strictEqual(testObject.verificationStatus, true); - assert.strictEqual(testObject.installed, true); - }); - - test('if verification is disabled by setting set to false, the task skips verification', async () => { - const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader({ isSignatureVerificationEnabled: false, verificationResult: 'error' }), disposables.add(new DisposableStore())); - - await testObject.run(); - - assert.strictEqual(testObject.verificationStatus, false); - assert.strictEqual(testObject.installed, true); - }); - - test('if verification is disabled because the module is not loaded, the task skips verification', async () => { - const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader({ isSignatureVerificationEnabled: true, verificationResult: false }), disposables.add(new DisposableStore())); - - await testObject.run(); - - assert.strictEqual(testObject.verificationStatus, false); - assert.strictEqual(testObject.installed, true); - }); - - test('if verification fails to execute, the task completes', async () => { - const errorCode = 'ENOENT'; - const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader({ isSignatureVerificationEnabled: true, verificationResult: errorCode }), disposables.add(new DisposableStore())); - - await testObject.run(); - - assert.strictEqual(testObject.verificationStatus, errorCode); - assert.strictEqual(testObject.installed, true); - }); - - test('if verification fails', async () => { - const errorCode = 'IntegrityCheckFailed'; - - const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader({ isSignatureVerificationEnabled: true, verificationResult: errorCode }), disposables.add(new DisposableStore())); - - await testObject.run(); - - assert.strictEqual(testObject.verificationStatus, errorCode); - assert.strictEqual(testObject.installed, true); - }); - - test('if verification succeeds, the task completes', async () => { - const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader({ isSignatureVerificationEnabled: true, verificationResult: true }), disposables.add(new DisposableStore())); - - await testObject.run(); - - assert.strictEqual(testObject.verificationStatus, true); - assert.strictEqual(testObject.installed, true); - }); - - test('task completes for unsigned extension', async () => { - const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: false }), anExtensionsDownloader({ isSignatureVerificationEnabled: true, verificationResult: true }), disposables.add(new DisposableStore())); - - await testObject.run(); - - assert.strictEqual(testObject.verificationStatus, false); - assert.strictEqual(testObject.installed, true); - }); - - test('task completes for an unsigned extension even when signature verification throws error', async () => { - const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: false }), anExtensionsDownloader({ isSignatureVerificationEnabled: true, verificationResult: 'error' }), disposables.add(new DisposableStore())); - - await testObject.run(); - - assert.strictEqual(testObject.verificationStatus, false); - assert.strictEqual(testObject.installed, true); - }); - - function anExtensionsDownloader(options: { isSignatureVerificationEnabled: boolean; verificationResult: boolean | string; quality?: string }): ExtensionsDownloader { - const logService = new NullLogService(); - const fileService = disposables.add(new FileService(logService)); - const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider()); - disposables.add(fileService.registerProvider(ROOT.scheme, fileSystemProvider)); - - const instantiationService = disposables.add(new TestInstantiationService()); - instantiationService.stub(IProductService, { quality: options.quality ?? 'insiders' }); - instantiationService.stub(IFileService, fileService); - instantiationService.stub(ILogService, logService); - instantiationService.stub(INativeEnvironmentService, { extensionsDownloadLocation: joinPath(ROOT, 'CachedExtensionVSIXs') }); - instantiationService.stub(IExtensionGalleryService, { - async download(extension, location, operation) { - await fileService.writeFile(location, VSBuffer.fromString('extension vsix')); - }, - async downloadSignatureArchive(extension, location) { - await fileService.writeFile(location, VSBuffer.fromString('extension signature')); - }, - }); - instantiationService.stub(IConfigurationService, new TestConfigurationService(isBoolean(options.isSignatureVerificationEnabled) ? { extensions: { verifySignature: options.isSignatureVerificationEnabled } } : undefined)); - instantiationService.stub(IExtensionSignatureVerificationService, new TestExtensionSignatureVerificationService(options.verificationResult)); - return disposables.add(instantiationService.createInstance(TestExtensionDownloader)); - } - - function aGalleryExtension(name: string, properties: Partial = {}, galleryExtensionProperties: any = {}, assets: Partial = {}): IGalleryExtension { - const targetPlatform = getTargetPlatform(platform, arch); - const galleryExtension = Object.create({ name, publisher: 'pub', version: '1.0.0', allTargetPlatforms: [targetPlatform], properties: {}, assets: {}, ...properties }); - galleryExtension.properties = { ...galleryExtension.properties, dependencies: [], targetPlatform, ...galleryExtensionProperties }; - galleryExtension.assets = { ...galleryExtension.assets, ...assets }; - galleryExtension.identifier = { id: getGalleryExtensionId(galleryExtension.publisher, galleryExtension.name), uuid: generateUuid() }; - return galleryExtension; - } -}); From e65febca09c77129d5e45fcf90886437e69f0441 Mon Sep 17 00:00:00 2001 From: Robo Date: Fri, 17 May 2024 00:18:57 +0900 Subject: [PATCH 236/357] ci: use sysroots for oss linux pipeline (#212895) * ci: use sysroots for oss linux pipeline * ci: update cache * ci: cleanup conditions --- build/.cachesalt | 2 +- .../linux/product-build-linux.yml | 97 ++++++++----------- 2 files changed, 43 insertions(+), 56 deletions(-) diff --git a/build/.cachesalt b/build/.cachesalt index 8148922f086..a454f1220da 100644 --- a/build/.cachesalt +++ b/build/.cachesalt @@ -1 +1 @@ -2024-05-16T08:47:22.277Z +2024-05-16T14:24:05.381Z diff --git a/build/azure-pipelines/linux/product-build-linux.yml b/build/azure-pipelines/linux/product-build-linux.yml index cdc687fe7ac..352b31360f8 100644 --- a/build/azure-pipelines/linux/product-build-linux.yml +++ b/build/azure-pipelines/linux/product-build-linux.yml @@ -100,50 +100,48 @@ steps: condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) displayName: Setup NPM Authentication + - script: | + set -e + + for i in {1..5}; do # try 5 times + yarn --cwd build --frozen-lockfile --check-files && break + if [ $i -eq 3 ]; then + echo "Yarn failed too many times" >&2 + exit 1 + fi + echo "Yarn failed $i, trying again..." + done + + source ./build/azure-pipelines/linux/setup-env.sh + + for i in {1..5}; do # try 5 times + yarn --frozen-lockfile --check-files && break + if [ $i -eq 3 ]; then + echo "Yarn failed too many times" >&2 + exit 1 + fi + echo "Yarn failed $i, trying again..." + done + env: + npm_config_arch: $(NPM_ARCH) + VSCODE_ARCH: $(VSCODE_ARCH) + ELECTRON_SKIP_BINARY_DOWNLOAD: 1 + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + GITHUB_TOKEN: "$(github-distro-mixin-password)" + displayName: Install dependencies + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) + + - script: | + set -e + + EXPECTED_GLIBC_VERSION="2.28" \ + EXPECTED_GLIBCXX_VERSION="3.4.25" \ + ./build/azure-pipelines/linux/verify-glibc-requirements.sh + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) + displayName: Check GLIBC and GLIBCXX dependencies in remote/node_modules + + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: - - script: | - set -e - # To workaround the issue of yarn not respecting the registry value from .npmrc - yarn config set registry "$NPM_REGISTRY" - - for i in {1..5}; do # try 5 times - yarn --cwd build --frozen-lockfile --check-files && break - if [ $i -eq 3 ]; then - echo "Yarn failed too many times" >&2 - exit 1 - fi - echo "Yarn failed $i, trying again..." - done - - source ./build/azure-pipelines/linux/setup-env.sh - - for i in {1..5}; do # try 5 times - yarn --frozen-lockfile --check-files && break - if [ $i -eq 3 ]; then - echo "Yarn failed too many times" >&2 - exit 1 - fi - echo "Yarn failed $i, trying again..." - done - env: - npm_config_arch: $(NPM_ARCH) - VSCODE_ARCH: $(VSCODE_ARCH) - NPM_REGISTRY: "$(NPM_REGISTRY)" - ELECTRON_SKIP_BINARY_DOWNLOAD: 1 - PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - GITHUB_TOKEN: "$(github-distro-mixin-password)" - displayName: Install dependencies (non-OSS) - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - - - script: | - set -e - - EXPECTED_GLIBC_VERSION="2.28" \ - EXPECTED_GLIBCXX_VERSION="3.4.25" \ - ./build/azure-pipelines/linux/verify-glibc-requirements.sh - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - displayName: Check GLIBC and GLIBCXX dependencies in remote/node_modules - - script: node build/azure-pipelines/distro/mixin-npm condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) displayName: Mixin distro node modules @@ -154,23 +152,12 @@ steps: - script: | set -e - for i in {1..5}; do # try 5 times - yarn --frozen-lockfile --check-files && break - if [ $i -eq 3 ]; then - echo "Yarn failed too many times" >&2 - exit 1 - fi - echo "Yarn failed $i, trying again..." - done - cd node_modules/native-keymap && npx node-gyp@9.4.0 -y rebuild --debug cd ../.. && ./.github/workflows/check-clean-git-state.sh env: npm_config_arch: $(NPM_ARCH) - ELECTRON_SKIP_BINARY_DOWNLOAD: 1 - PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 GITHUB_TOKEN: "$(github-distro-mixin-password)" - displayName: Install dependencies (OSS) + displayName: Rebuild debug version of native modules (OSS) condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - script: | From 2d97803568f6942350a2e45a897ab2f1ab530c66 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 16 May 2024 17:29:17 +0200 Subject: [PATCH 237/357] add `ListenerRefusalError` and `ListenerLeakError` which get logged when listener thresholds are exceeded. (#212900) * add `ListenerRefusalError` and `ListenerLeakError` which get logged when listener thresholds are exceeded. The `stack` property of these errors will point towards the most frequent listener and how often it is used. If that's a high number there is a leak (same listener is added over and over again), if that's a low number there might be a conceptual flaw that an emitter is simply too prominent. * rightfully don't use Error.captureStackTrace (v8/nodejs only) --- src/vs/base/common/event.ts | 68 +++++++++++++++++++++------ src/vs/base/test/common/event.test.ts | 28 ++++++++++- test/unit/electron/renderer.js | 3 +- 3 files changed, 83 insertions(+), 16 deletions(-) diff --git a/src/vs/base/common/event.ts b/src/vs/base/common/event.ts index e468ba9b780..7cefcdcedbd 100644 --- a/src/vs/base/common/event.ts +++ b/src/vs/base/common/event.ts @@ -835,6 +835,7 @@ class LeakageMonitor { private _warnCountdown: number = 0; constructor( + private readonly _errorHandler: (err: Error) => void, readonly threshold: number, readonly name: string = Math.random().toString(18).slice(2, 5), ) { } @@ -862,18 +863,13 @@ class LeakageMonitor { // is exceeded by 50% again this._warnCountdown = threshold * 0.5; - // find most frequent listener and print warning - let topStack: string | undefined; - let topCount: number = 0; - for (const [stack, count] of this._stacks) { - if (!topStack || topCount < count) { - topStack = stack; - topCount = count; - } - } - - console.warn(`[${this.name}] potential listener LEAK detected, having ${listenerCount} listeners already. MOST frequent listener (${topCount}):`); + const [topStack, topCount] = this.getMostFrequentStack()!; + const message = `[${this.name}] potential listener LEAK detected, having ${listenerCount} listeners already. MOST frequent listener (${topCount}):`; + console.warn(message); console.warn(topStack!); + + const error = new ListenerLeakError(message, topStack); + this._errorHandler(error); } return () => { @@ -881,12 +877,28 @@ class LeakageMonitor { this._stacks!.set(stack.value, count - 1); }; } + + getMostFrequentStack(): [string, number] | undefined { + if (!this._stacks) { + return undefined; + } + let topStack: [string, number] | undefined; + let topCount: number = 0; + for (const [stack, count] of this._stacks) { + if (!topStack || topCount < count) { + topStack = [stack, count]; + topCount = count; + } + } + return topStack; + } } class Stacktrace { static create() { - return new Stacktrace(new Error().stack ?? ''); + const err = new Error(); + return new Stacktrace(err.stack ?? ''); } private constructor(readonly value: string) { } @@ -896,6 +908,25 @@ class Stacktrace { } } +// error that is logged when going over the configured listener threshold +export class ListenerLeakError extends Error { + constructor(message: string, stack: string) { + super(message); + this.name = 'ListenerLeakError'; + this.stack = stack; + } +} + +// SEVERE error that is logged when having gone way over the configured listener +// threshold so that the emitter refuses to accept more listeners +export class ListenerRefusalError extends Error { + constructor(message: string, stack: string) { + super(message); + this.name = 'ListenerRefusalError'; + this.stack = stack; + } +} + let id = 0; class UniqueContainer { stack?: Stacktrace; @@ -988,7 +1019,9 @@ export class Emitter { constructor(options?: EmitterOptions) { this._options = options; - this._leakageMon = _globalLeakWarningThreshold > 0 || this._options?.leakWarningThreshold ? new LeakageMonitor(this._options?.leakWarningThreshold ?? _globalLeakWarningThreshold) : undefined; + this._leakageMon = (_globalLeakWarningThreshold > 0 || this._options?.leakWarningThreshold) + ? new LeakageMonitor(options?.onListenerError ?? onUnexpectedError, this._options?.leakWarningThreshold ?? _globalLeakWarningThreshold) : + undefined; this._perfMon = this._options?._profName ? new EventProfiling(this._options._profName) : undefined; this._deliveryQueue = this._options?.deliveryQueue as EventDeliveryQueuePrivate | undefined; } @@ -1033,7 +1066,14 @@ export class Emitter { get event(): Event { this._event ??= (callback: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => { if (this._leakageMon && this._size > this._leakageMon.threshold * 3) { - console.warn(`[${this._leakageMon.name}] REFUSES to accept new listeners because it exceeded its threshold by far`); + const message = `[${this._leakageMon.name}] REFUSES to accept new listeners because it exceeded its threshold by far (${this._size}/${this._leakageMon.threshold})`; + console.warn(message); + + const tuple = this._leakageMon.getMostFrequentStack() ?? ['UNKNOWN stack', -1]; + const error = new ListenerRefusalError(`${message}. HINT: Stack shows most frequent listener (${tuple[1]}-times)`, tuple[0]); + const errorHandler = this._options?.onListenerError || onUnexpectedError; + errorHandler(error); + return Disposable.None; } diff --git a/src/vs/base/test/common/event.test.ts b/src/vs/base/test/common/event.test.ts index 49962c89d5a..7c33d1f41af 100644 --- a/src/vs/base/test/common/event.test.ts +++ b/src/vs/base/test/common/event.test.ts @@ -4,10 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; import { stub } from 'sinon'; +import { tail2 } from 'vs/base/common/arrays'; import { DeferredPromise, timeout } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { errorHandler, setUnexpectedErrorHandler } from 'vs/base/common/errors'; -import { AsyncEmitter, DebounceEmitter, DynamicListEventMultiplexer, Emitter, Event, EventBufferer, EventMultiplexer, IWaitUntil, MicrotaskEmitter, PauseableEmitter, Relay, createEventDeliveryQueue } from 'vs/base/common/event'; +import { AsyncEmitter, DebounceEmitter, DynamicListEventMultiplexer, Emitter, Event, EventBufferer, EventMultiplexer, IWaitUntil, ListenerLeakError, ListenerRefusalError, MicrotaskEmitter, PauseableEmitter, Relay, createEventDeliveryQueue } from 'vs/base/common/event'; import { DisposableStore, IDisposable, isDisposable, setDisposableTracker, DisposableTracker } from 'vs/base/common/lifecycle'; import { observableValue, transaction } from 'vs/base/common/observable'; import { MicrotaskDelay } from 'vs/base/common/symbols'; @@ -368,6 +369,31 @@ suite('Event', function () { }); + test('throw ListenerLeakError', () => { + + const store = new DisposableStore(); + const allError: any[] = []; + + const a = ds.add(new Emitter({ + onListenerError(e) { allError.push(e); }, + leakWarningThreshold: 1, + })); + + for (let i = 0; i < 5; i++) { + a.event(() => { }, undefined, store); + } + + assert.deepStrictEqual(allError.length, 4); + const [start, tail] = tail2(allError); + assert.ok(tail instanceof ListenerRefusalError); + + for (const item of start) { + assert.ok(item instanceof ListenerLeakError); + } + + store.dispose(); + }); + test('reusing event function and context', function () { let counter = 0; function listener() { diff --git a/test/unit/electron/renderer.js b/test/unit/electron/renderer.js index 26e45f9ff9d..c9f4dfa1e6a 100644 --- a/test/unit/electron/renderer.js +++ b/test/unit/electron/renderer.js @@ -199,7 +199,8 @@ async function loadTests(opts) { 'issue #149130: vscode freezes because of Bracket Pair Colorization', // https://github.com/microsoft/vscode/issues/192440 'property limits', // https://github.com/microsoft/vscode/issues/192443 'Error events', // https://github.com/microsoft/vscode/issues/192443 - 'fetch returns keybinding with user first if title and id matches' // + 'fetch returns keybinding with user first if title and id matches', // + 'throw ListenerLeakError' ]); let _testsWithUnexpectedOutput = false; From 32f970f937c4b5ee8ca7a997ad86e33f73d3dd87 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Thu, 16 May 2024 08:32:53 -0700 Subject: [PATCH 238/357] refactor: clean up redundant handle (#212859) Co-authored-by: Benjamin Pasero Co-authored-by: Johannes Rieken --- src/vs/workbench/api/common/extHostChatAgents2.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 455377b75dd..c39bf7741b0 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -33,7 +33,6 @@ class ChatAgentResponseStream { private _isClosed: boolean = false; private _firstProgress: number | undefined; private _apiObject: vscode.ChatResponseStream | undefined; - private progressReporterHandlePool = 0; constructor( private readonly _extension: IExtensionDescription, @@ -69,13 +68,13 @@ class ChatAgentResponseStream { } } - const _report = (progress: IChatProgressDto, task?: (progress: vscode.Progress) => Thenable, progressReporterHandle?: number) => { + const _report = (progress: IChatProgressDto, task?: (progress: vscode.Progress) => Thenable) => { // Measure the time to the first progress update with real markdown content if (typeof this._firstProgress === 'undefined' && 'content' in progress) { this._firstProgress = this._stopWatch.elapsed(); } - if (progressReporterHandle !== undefined) { + if (task) { const progressReporterPromise = this._proxy.$handleProgressChunk(this._request.requestId, progress); const progressReporter = { report: (p: vscode.ChatResponseWarningPart | vscode.ChatResponseReferencePart) => { @@ -145,7 +144,7 @@ class ChatAgentResponseStream { throwIfDone(this.progress); const part = new extHostTypes.ChatResponseProgressPart2(value, task); const dto = task ? typeConvert.ChatTask.from(part) : typeConvert.ChatResponseProgressPart.from(part); - _report(dto, task, that.progressReporterHandlePool++); + _report(dto, task); return this; }, warning(value) { From 2ef785bb00545c3f175d8983d4537eb01ef09b7d Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 16 May 2024 17:53:33 +0200 Subject: [PATCH 239/357] make sure to use `ScrollType.Immediate` when reveal inline chat content widget (#212906) fixes https://github.com/microsoft/vscode-copilot/issues/5573 --- .../contrib/inlineChat/browser/inlineChatContentWidget.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.ts index 8a64d210ece..b68ae908369 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.ts @@ -22,6 +22,7 @@ import { Range } from 'vs/editor/common/core/range'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { ScrollType } from 'vs/editor/common/editorCommon'; export class InlineChatContentWidget implements IContentWidget { @@ -168,7 +169,7 @@ export class InlineChatContentWidget implements IContentWidget { this._visible = true; this._focusNext = true; - this._editor.revealRangeNearTopIfOutsideViewport(Range.fromPositions(position)); + this._editor.revealRangeNearTopIfOutsideViewport(Range.fromPositions(position), ScrollType.Immediate); this._widget.inputEditor.setValue(''); const wordInfo = this._editor.getModel()?.getWordAtPosition(position); From a9e0dbdfc518a16e12291aebc0fc0a50a852e174 Mon Sep 17 00:00:00 2001 From: Simon Siefke Date: Thu, 16 May 2024 18:22:58 +0200 Subject: [PATCH 240/357] feature: enable isolated modules --- src/tsconfig.json | 1 + src/typings/require.d.ts | 2 +- src/vs/workbench/contrib/tasks/common/taskService.ts | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/tsconfig.json b/src/tsconfig.json index 1e0c8b14a1a..2ac819490e5 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -6,6 +6,7 @@ "sourceMap": false, "allowJs": true, "resolveJsonModule": true, + "isolatedModules": true, "outDir": "../out/vs", "types": [ "mocha", diff --git a/src/typings/require.d.ts b/src/typings/require.d.ts index 3fda6d6981d..f051253046f 100644 --- a/src/typings/require.d.ts +++ b/src/typings/require.d.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -declare const enum LoaderEventType { +declare enum LoaderEventType { LoaderAvailable = 1, BeginLoadingScript = 10, diff --git a/src/vs/workbench/contrib/tasks/common/taskService.ts b/src/vs/workbench/contrib/tasks/common/taskService.ts index 3ef60f6e047..3db39c2267d 100644 --- a/src/vs/workbench/contrib/tasks/common/taskService.ts +++ b/src/vs/workbench/contrib/tasks/common/taskService.ts @@ -15,7 +15,7 @@ import { ITaskSummary, ITaskTerminateResponse, ITaskSystemInfo } from 'vs/workbe import { IStringDictionary } from 'vs/base/common/collections'; import { RawContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -export { ITaskSummary, Task, ITaskTerminateResponse as TaskTerminateResponse }; +export type { ITaskSummary, Task, ITaskTerminateResponse as TaskTerminateResponse }; export const CustomExecutionSupportedContext = new RawContextKey('customExecutionSupported', false, nls.localize('tasks.customExecutionSupported', "Whether CustomExecution tasks are supported. Consider using in the when clause of a \'taskDefinition\' contribution.")); export const ShellExecutionSupportedContext = new RawContextKey('shellExecutionSupported', false, nls.localize('tasks.shellExecutionSupported', "Whether ShellExecution tasks are supported. Consider using in the when clause of a \'taskDefinition\' contribution.")); From 15b40c8e0dcb9703bf354400919e9e7ad33f6a67 Mon Sep 17 00:00:00 2001 From: Simon Siefke Date: Thu, 16 May 2024 18:24:08 +0200 Subject: [PATCH 241/357] use type exports --- src/vs/base/node/processes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/base/node/processes.ts b/src/vs/base/node/processes.ts index a71a7b8cb4e..9c07f106711 100644 --- a/src/vs/base/node/processes.ts +++ b/src/vs/base/node/processes.ts @@ -11,7 +11,7 @@ import * as process from 'vs/base/common/process'; import { CommandOptions, ForkOptions, Source, SuccessData, TerminateResponse, TerminateResponseCode } from 'vs/base/common/processes'; import * as Types from 'vs/base/common/types'; import * as pfs from 'vs/base/node/pfs'; -export { CommandOptions, ForkOptions, SuccessData, Source, TerminateResponse, TerminateResponseCode }; +export { type CommandOptions, type ForkOptions, type SuccessData, Source, type TerminateResponse, TerminateResponseCode }; export type ValueCallback = (value: T | Promise) => void; export type ErrorCallback = (error?: any) => void; From 0f1ddcec99ec81d79439484190eeafca918d1556 Mon Sep 17 00:00:00 2001 From: Simon Siefke Date: Thu, 16 May 2024 18:26:23 +0200 Subject: [PATCH 242/357] move edit mode --- .../workbench/contrib/inlineChat/common/inlineChat.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index fc2da4a247d..714f1e07547 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -148,6 +148,11 @@ export interface IInlineChatService { export const INLINE_CHAT_ID = 'interactiveEditor'; export const INTERACTIVE_EDITOR_ACCESSIBILITY_HELP_ID = 'interactiveEditorAccessiblityHelp'; +export const enum EditMode { + Live = 'live', + Preview = 'preview' +} + export const CTX_INLINE_CHAT_HAS_PROVIDER = new RawContextKey('inlineChatHasProvider', false, localize('inlineChatHasProvider', "Whether a provider for interactive editors exists")); export const CTX_INLINE_CHAT_VISIBLE = new RawContextKey('inlineChatVisible', false, localize('inlineChatVisible', "Whether the interactive editor input is visible")); export const CTX_INLINE_CHAT_FOCUSED = new RawContextKey('inlineChatFocused', false, localize('inlineChatFocused', "Whether the interactive editor input is focused")); @@ -206,10 +211,7 @@ export const overviewRulerInlineChatDiffRemoved = registerColor('editorOverviewR // settings -export const enum EditMode { - Live = 'live', - Preview = 'preview' -} + Registry.as(ExtensionsMigration.ConfigurationMigration).registerConfigurationMigrations( [{ From b435d3fbe1b5bfa647dc0075f8b486c3ff7d6124 Mon Sep 17 00:00:00 2001 From: Simon Siefke Date: Thu, 16 May 2024 18:42:11 +0200 Subject: [PATCH 243/357] update setting export --- .../accessibility/browser/accessibilityConfiguration.ts | 9 +++------ src/vs/workbench/contrib/speech/common/speechService.ts | 8 +++++++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts index f5f1a703371..daa28a1342c 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts @@ -9,7 +9,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { workbenchConfigurationNodeBase, Extensions as WorkbenchExtensions, IConfigurationMigrationRegistry, ConfigurationKeyValuePairs, ConfigurationMigration } from 'vs/workbench/common/configuration'; import { AccessibilitySignal } from 'vs/platform/accessibilitySignal/browser/accessibilitySignalService'; -import { ISpeechService, SPEECH_LANGUAGES, SPEECH_LANGUAGE_CONFIG } from 'vs/workbench/contrib/speech/common/speechService'; +import { AccessibilityVoiceSettingId, ISpeechService, SPEECH_LANGUAGES, SPEECH_LANGUAGE_CONFIG } from 'vs/workbench/contrib/speech/common/speechService'; import { Disposable } from 'vs/base/common/lifecycle'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { Event } from 'vs/base/common/event'; @@ -644,11 +644,8 @@ export function registerAccessibilityConfiguration() { }); } -export const enum AccessibilityVoiceSettingId { - SpeechTimeout = 'accessibility.voice.speechTimeout', - AutoSynthesize = 'accessibility.voice.autoSynthesize', - SpeechLanguage = SPEECH_LANGUAGE_CONFIG -} +export { AccessibilityVoiceSettingId } + export const SpeechTimeoutDefault = 1200; export class DynamicSpeechAccessibilityConfiguration extends Disposable implements IWorkbenchContribution { diff --git a/src/vs/workbench/contrib/speech/common/speechService.ts b/src/vs/workbench/contrib/speech/common/speechService.ts index b915e7d394f..0b99c46ac57 100644 --- a/src/vs/workbench/contrib/speech/common/speechService.ts +++ b/src/vs/workbench/contrib/speech/common/speechService.ts @@ -134,7 +134,13 @@ export interface ISpeechService { recognizeKeyword(token: CancellationToken): Promise; } -export const SPEECH_LANGUAGE_CONFIG = 'accessibility.voice.speechLanguage'; +export const enum AccessibilityVoiceSettingId { + SpeechTimeout = 'accessibility.voice.speechTimeout', + AutoSynthesize = 'accessibility.voice.autoSynthesize', + SpeechLanguage = 'accessibility.voice.speechLanguage', +} + +export const SPEECH_LANGUAGE_CONFIG = AccessibilityVoiceSettingId.SpeechLanguage; export const SPEECH_LANGUAGES = { ['da-DK']: { From 087d09ed1e502fd66eb48a56d8a83f829beb7dd1 Mon Sep 17 00:00:00 2001 From: Simon Siefke Date: Thu, 16 May 2024 18:43:19 +0200 Subject: [PATCH 244/357] remove unused export --- .../contrib/accessibility/browser/accessibilityConfiguration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts index daa28a1342c..5f36624a2ad 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts @@ -9,7 +9,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { workbenchConfigurationNodeBase, Extensions as WorkbenchExtensions, IConfigurationMigrationRegistry, ConfigurationKeyValuePairs, ConfigurationMigration } from 'vs/workbench/common/configuration'; import { AccessibilitySignal } from 'vs/platform/accessibilitySignal/browser/accessibilitySignalService'; -import { AccessibilityVoiceSettingId, ISpeechService, SPEECH_LANGUAGES, SPEECH_LANGUAGE_CONFIG } from 'vs/workbench/contrib/speech/common/speechService'; +import { AccessibilityVoiceSettingId, ISpeechService, SPEECH_LANGUAGES } from 'vs/workbench/contrib/speech/common/speechService'; import { Disposable } from 'vs/base/common/lifecycle'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { Event } from 'vs/base/common/event'; From cdf94536b9dafc36582d2ecfd246897c6feb7d72 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 16 May 2024 10:39:19 -0700 Subject: [PATCH 245/357] update screen readers with plaintext version of response content (#212916) fix #212788 --- .../contrib/chat/browser/chatAccessibilityService.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts b/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts index aa7c612e090..304fb89dbc9 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts @@ -10,6 +10,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { AccessibilityProgressSignalScheduler } from 'vs/platform/accessibilitySignal/browser/progressAccessibilitySignalScheduler'; import { IChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; +import { renderStringAsPlaintext } from 'vs/base/browser/markdownRenderer'; const CHAT_RESPONSE_PENDING_ALLOWANCE_MS = 4000; export class ChatAccessibilityService extends Disposable implements IChatAccessibilityService { @@ -34,11 +35,11 @@ export class ChatAccessibilityService extends Disposable implements IChatAccessi const isPanelChat = typeof response !== 'string'; const responseContent = typeof response === 'string' ? response : response?.response.asString(); this._accessibilitySignalService.playSignal(AccessibilitySignal.chatResponseReceived, { allowManyInParallel: true }); - if (!response) { + if (!response || !responseContent) { return; } const errorDetails = isPanelChat && response.errorDetails ? ` ${response.errorDetails.message}` : ''; - status(responseContent + errorDetails); + status(renderStringAsPlaintext(responseContent) + errorDetails); } } From 60d035d19aeb81b9dfe301165cab65f9cc40b0d1 Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Thu, 16 May 2024 11:14:59 -0700 Subject: [PATCH 246/357] issue reporter refactor (web prep) (#212762) * kinda beeeg refactor * swap the extended issue reporteR * update PR commentsgit add .! --- src/vs/code/browser/issue/issue.ts | 1205 +++++++++++++++++ .../issue/issueReporterModel.ts | 0 .../issue/issueReporterPage.ts | 0 .../browser/issue/issueReporterService.ts | 203 +++ .../issue/issueReporterMain.ts | 6 +- .../issue/issueReporterService.ts | 2 +- .../issue/issueReporterService2.ts | 508 +++++++ .../issue/testReporterModel.test.ts | 2 +- 8 files changed, 1921 insertions(+), 5 deletions(-) create mode 100644 src/vs/code/browser/issue/issue.ts rename src/vs/code/{electron-sandbox => browser}/issue/issueReporterModel.ts (100%) rename src/vs/code/{electron-sandbox => browser}/issue/issueReporterPage.ts (100%) create mode 100644 src/vs/code/browser/issue/issueReporterService.ts create mode 100644 src/vs/code/electron-sandbox/issue/issueReporterService2.ts diff --git a/src/vs/code/browser/issue/issue.ts b/src/vs/code/browser/issue/issue.ts new file mode 100644 index 00000000000..cfbf5c75b7f --- /dev/null +++ b/src/vs/code/browser/issue/issue.ts @@ -0,0 +1,1205 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { Disposable } from 'vs/base/common/lifecycle'; +import { IProductConfiguration } from 'vs/base/common/product'; +import { $, createStyleSheet, reset, windowOpenNoOpener } from 'vs/base/browser/dom'; +import { Button, unthemedButtonStyles } from 'vs/base/browser/ui/button/button'; +import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; +import { Delayer, RunOnceScheduler } from 'vs/base/common/async'; +import { Codicon } from 'vs/base/common/codicons'; +import { debounce } from 'vs/base/common/decorators'; +import { CancellationError } from 'vs/base/common/errors'; +import { isLinuxSnap } from 'vs/base/common/platform'; +import { escape } from 'vs/base/common/strings'; +import { URI } from 'vs/base/common/uri'; +import { IssueReporterModel, IssueReporterData as IssueReporterModelData } from 'vs/code/browser/issue/issueReporterModel'; +import { localize } from 'vs/nls'; +import { IIssueMainService, IssueReporterData, IssueReporterExtensionData, IssueReporterStyles, IssueType } from 'vs/platform/issue/common/issue'; +import { normalizeGitHubUrl } from 'vs/platform/issue/common/issueReporterUtil'; +import { getIconsStyleSheet } from 'vs/platform/theme/browser/iconsStyleSheet'; + +const MAX_URL_LENGTH = 7500; + +interface SearchResult { + html_url: string; + title: string; + state?: string; +} + +enum IssueSource { + VSCode = 'vscode', + Extension = 'extension', + Marketplace = 'marketplace', + Unknown = 'unknown' +} + + +export class BaseIssueReporterService extends Disposable { + public issueReporterModel: IssueReporterModel; + public receivedSystemInfo = false; + public numberOfSearchResultsDisplayed = 0; + public receivedPerformanceInfo = false; + public shouldQueueSearch = false; + public hasBeenSubmitted = false; + public openReporter = false; + public loadingExtensionData = false; + public selectedExtension = ''; + public delayedSubmit = new Delayer(300); + public previewButton!: Button; + public nonGitHubIssueUrl = false; + + constructor( + public disableExtensions: boolean, + public data: IssueReporterData, + public os: { + type: string; + arch: string; + release: string; + }, + public product: IProductConfiguration, + public readonly window: Window, + public readonly isWeb: boolean, + @IIssueMainService public readonly issueMainService: IIssueMainService + ) { + super(); + const targetExtension = data.extensionId ? data.enabledExtensions.find(extension => extension.id.toLocaleLowerCase() === data.extensionId?.toLocaleLowerCase()) : undefined; + this.issueReporterModel = new IssueReporterModel({ + ...data, + issueType: data.issueType || IssueType.Bug, + versionInfo: { + vscodeVersion: `${product.nameShort} ${!!product.darwinUniversalAssetId ? `${product.version} (Universal)` : product.version} (${product.commit || 'Commit unknown'}, ${product.date || 'Date unknown'})`, + os: `${this.os.type} ${this.os.arch} ${this.os.release}${isLinuxSnap ? ' snap' : ''}` + }, + extensionsDisabled: !!this.disableExtensions, + fileOnExtension: data.extensionId ? !targetExtension?.isBuiltin : undefined, + selectedExtension: targetExtension + }); + + const fileOnMarketplace = data.issueSource === IssueSource.Marketplace; + const fileOnProduct = data.issueSource === IssueSource.VSCode; + this.issueReporterModel.update({ fileOnMarketplace, fileOnProduct }); + + //TODO: Handle case where extension is not activated + const issueReporterElement = this.getElementById('issue-reporter'); + if (issueReporterElement) { + this.previewButton = new Button(issueReporterElement, unthemedButtonStyles); + const issueRepoName = document.createElement('a'); + issueReporterElement.appendChild(issueRepoName); + issueRepoName.id = 'show-repo-name'; + issueRepoName.classList.add('hidden'); + this.updatePreviewButtonState(); + } + + const issueTitle = data.issueTitle; + if (issueTitle) { + const issueTitleElement = this.getElementById('issue-title'); + if (issueTitleElement) { + issueTitleElement.value = issueTitle; + } + } + + const issueBody = data.issueBody; + if (issueBody) { + const description = this.getElementById('description'); + if (description) { + description.value = issueBody; + this.issueReporterModel.update({ issueDescription: issueBody }); + } + } + + if (this.window.document.documentElement.lang !== 'en') { + show(this.getElementById('english')); + } + + const codiconStyleSheet = createStyleSheet(); + codiconStyleSheet.id = 'codiconStyles'; + + // TODO: Is there a way to use the IThemeService here instead + const iconsStyleSheet = this._register(getIconsStyleSheet(undefined)); + function updateAll() { + codiconStyleSheet.textContent = iconsStyleSheet.getCSS(); + } + + const delayer = new RunOnceScheduler(updateAll, 0); + iconsStyleSheet.onDidChange(() => delayer.schedule()); + delayer.schedule(); + + this.setUpTypes(); + this.applyStyles(data.styles); + + // Handle case where extension is pre-selected through the command + if ((data.data || data.uri) && targetExtension) { + this.updateExtensionStatus(targetExtension); + } + } + + render(): void { + this.renderBlocks(); + } + + setInitialFocus() { + const { fileOnExtension } = this.issueReporterModel.getData(); + if (fileOnExtension) { + const issueTitle = this.window.document.getElementById('issue-title'); + issueTitle?.focus(); + } else { + const issueType = this.window.document.getElementById('issue-type'); + issueType?.focus(); + } + } + + // TODO @justschen: After migration to Aux Window, switch to dedicated css. + private applyStyles(styles: IssueReporterStyles) { + const styleTag = document.createElement('style'); + const content: string[] = []; + + if (styles.inputBackground) { + content.push(`input[type="text"], textarea, select, .issues-container > .issue > .issue-state, .block-info { background-color: ${styles.inputBackground}; }`); + } + + if (styles.inputBorder) { + content.push(`input[type="text"], textarea, select { border: 1px solid ${styles.inputBorder}; }`); + } else { + content.push(`input[type="text"], textarea, select { border: 1px solid transparent; }`); + } + + if (styles.inputForeground) { + content.push(`input[type="text"], textarea, select, .issues-container > .issue > .issue-state, .block-info { color: ${styles.inputForeground}; }`); + } + + if (styles.inputErrorBorder) { + content.push(`.invalid-input, .invalid-input:focus, .validation-error { border: 1px solid ${styles.inputErrorBorder} !important; }`); + content.push(`.required-input { color: ${styles.inputErrorBorder}; }`); + } + + if (styles.inputErrorBackground) { + content.push(`.validation-error { background: ${styles.inputErrorBackground}; }`); + } + + if (styles.inputErrorForeground) { + content.push(`.validation-error { color: ${styles.inputErrorForeground}; }`); + } + + if (styles.inputActiveBorder) { + content.push(`input[type='text']:focus, textarea:focus, select:focus, summary:focus, button:focus, a:focus, .workbenchCommand:focus { border: 1px solid ${styles.inputActiveBorder}; outline-style: none; }`); + } + + if (styles.textLinkColor) { + content.push(`a, .workbenchCommand { color: ${styles.textLinkColor}; }`); + } + + if (styles.textLinkColor) { + content.push(`a { color: ${styles.textLinkColor}; }`); + } + + if (styles.textLinkActiveForeground) { + content.push(`a:hover, .workbenchCommand:hover { color: ${styles.textLinkActiveForeground}; }`); + } + + if (styles.sliderBackgroundColor) { + content.push(`::-webkit-scrollbar-thumb { background-color: ${styles.sliderBackgroundColor}; }`); + } + + if (styles.sliderActiveColor) { + content.push(`::-webkit-scrollbar-thumb:active { background-color: ${styles.sliderActiveColor}; }`); + } + + if (styles.sliderHoverColor) { + content.push(`::--webkit-scrollbar-thumb:hover { background-color: ${styles.sliderHoverColor}; }`); + } + + if (styles.buttonBackground) { + content.push(`.monaco-text-button { background-color: ${styles.buttonBackground} !important; }`); + } + + if (styles.buttonForeground) { + content.push(`.monaco-text-button { color: ${styles.buttonForeground} !important; }`); + } + + if (styles.buttonHoverBackground) { + content.push(`.monaco-text-button:not(.disabled):hover, .monaco-text-button:focus { background-color: ${styles.buttonHoverBackground} !important; }`); + } + + styleTag.textContent = content.join('\n'); + this.window.document.head.appendChild(styleTag); + this.window.document.body.style.color = styles.color || ''; + } + + private async updateIssueReporterUri(extension: IssueReporterExtensionData): Promise { + try { + if (extension.uri) { + const uri = URI.revive(extension.uri); + extension.bugsUrl = uri.toString(); + } + } catch (e) { + this.renderBlocks(); + } + } + + public setEventHandlers(): void { + this.addEventListener('issue-type', 'change', (event: Event) => { + const issueType = parseInt((event.target).value); + this.issueReporterModel.update({ issueType: issueType }); + if (issueType === IssueType.PerformanceIssue && !this.receivedPerformanceInfo) { + this.issueMainService.$getPerformanceInfo().then(info => { + this.updatePerformanceInfo(info as Partial); + }); + } + + // Resets placeholder + const descriptionTextArea = this.getElementById('issue-title'); + if (descriptionTextArea) { + descriptionTextArea.placeholder = localize('undefinedPlaceholder', "Please enter a title"); + } + + this.updatePreviewButtonState(); + this.setSourceOptions(); + this.render(); + }); + + (['includeSystemInfo', 'includeProcessInfo', 'includeWorkspaceInfo', 'includeExtensions', 'includeExperiments', 'includeExtensionData'] as const).forEach(elementId => { + this.addEventListener(elementId, 'click', (event: Event) => { + event.stopPropagation(); + this.issueReporterModel.update({ [elementId]: !this.issueReporterModel.getData()[elementId] }); + }); + }); + + const showInfoElements = this.window.document.getElementsByClassName('showInfo'); + for (let i = 0; i < showInfoElements.length; i++) { + const showInfo = showInfoElements.item(i)!; + (showInfo as HTMLAnchorElement).addEventListener('click', (e: MouseEvent) => { + e.preventDefault(); + const label = (e.target); + if (label) { + const containingElement = label.parentElement && label.parentElement.parentElement; + const info = containingElement && containingElement.lastElementChild; + if (info && info.classList.contains('hidden')) { + show(info); + label.textContent = localize('hide', "hide"); + } else { + hide(info); + label.textContent = localize('show', "show"); + } + } + }); + } + + this.addEventListener('issue-source', 'change', (e: Event) => { + const value = (e.target).value; + const problemSourceHelpText = this.getElementById('problem-source-help-text')!; + if (value === '') { + this.issueReporterModel.update({ fileOnExtension: undefined }); + show(problemSourceHelpText); + this.clearSearchResults(); + this.render(); + return; + } else { + hide(problemSourceHelpText); + } + + const descriptionTextArea = this.getElementById('issue-title'); + if (value === IssueSource.VSCode) { + descriptionTextArea.placeholder = localize('vscodePlaceholder', "E.g Workbench is missing problems panel"); + } else if (value === IssueSource.Extension) { + descriptionTextArea.placeholder = localize('extensionPlaceholder', "E.g. Missing alt text on extension readme image"); + } else if (value === IssueSource.Marketplace) { + descriptionTextArea.placeholder = localize('marketplacePlaceholder', "E.g Cannot disable installed extension"); + } else { + descriptionTextArea.placeholder = localize('undefinedPlaceholder', "Please enter a title"); + } + + let fileOnExtension, fileOnMarketplace = false; + if (value === IssueSource.Extension) { + fileOnExtension = true; + } else if (value === IssueSource.Marketplace) { + fileOnMarketplace = true; + } + + this.issueReporterModel.update({ fileOnExtension, fileOnMarketplace }); + this.render(); + + const title = (this.getElementById('issue-title')).value; + this.searchIssues(title, fileOnExtension, fileOnMarketplace); + }); + + this.addEventListener('description', 'input', (e: Event) => { + const issueDescription = (e.target).value; + this.issueReporterModel.update({ issueDescription }); + + // Only search for extension issues on title change + if (this.issueReporterModel.fileOnExtension() === false) { + const title = (this.getElementById('issue-title')).value; + this.searchVSCodeIssues(title, issueDescription); + } + }); + + this.addEventListener('issue-title', 'input', (e: Event) => { + const title = (e.target).value; + const lengthValidationMessage = this.getElementById('issue-title-length-validation-error'); + const issueUrl = this.getIssueUrl(); + if (title && this.getIssueUrlWithTitle(title, issueUrl).length > MAX_URL_LENGTH) { + show(lengthValidationMessage); + } else { + hide(lengthValidationMessage); + } + const issueSource = this.getElementById('issue-source'); + if (!issueSource || issueSource.value === '') { + return; + } + + const { fileOnExtension, fileOnMarketplace } = this.issueReporterModel.getData(); + this.searchIssues(title, fileOnExtension, fileOnMarketplace); + }); + } + + public updatePerformanceInfo(info: Partial) { + this.issueReporterModel.update(info); + this.receivedPerformanceInfo = true; + + const state = this.issueReporterModel.getData(); + this.updateProcessInfo(state); + this.updateWorkspaceInfo(state); + this.updatePreviewButtonState(); + } + + public updatePreviewButtonState() { + if (this.isPreviewEnabled()) { + if (this.data.githubAccessToken) { + this.previewButton.label = localize('createOnGitHub', "Create on GitHub"); + } else { + this.previewButton.label = localize('previewOnGitHub', "Preview on GitHub"); + } + this.previewButton.enabled = true; + } else { + this.previewButton.enabled = false; + this.previewButton.label = localize('loadingData', "Loading data..."); + } + + const issueRepoName = this.getElementById('show-repo-name')! as HTMLAnchorElement; + const selectedExtension = this.issueReporterModel.getData().selectedExtension; + if (selectedExtension && selectedExtension.uri) { + const urlString = URI.revive(selectedExtension.uri).toString(); + issueRepoName.href = urlString; + issueRepoName.addEventListener('click', (e) => this.openLink(e)); + issueRepoName.addEventListener('auxclick', (e) => this.openLink(e)); + const gitHubInfo = this.parseGitHubUrl(urlString); + issueRepoName.textContent = gitHubInfo ? gitHubInfo.owner + '/' + gitHubInfo.repositoryName : urlString; + Object.assign(issueRepoName.style, { + alignSelf: 'flex-end', + display: 'block', + fontSize: '13px', + marginBottom: '10px', + padding: '4px 0px', + textDecoration: 'none', + width: 'auto' + }); + show(issueRepoName); + } else { + // clear styles + issueRepoName.removeAttribute('style'); + hide(issueRepoName); + } + + // Initial check when first opened. + this.getExtensionGitHubUrl(); + } + + private isPreviewEnabled() { + const issueType = this.issueReporterModel.getData().issueType; + + if (this.loadingExtensionData) { + return false; + } + + if (this.isWeb) { + if (issueType === IssueType.FeatureRequest || issueType === IssueType.PerformanceIssue || issueType === IssueType.Bug) { + return true; + } + } else { + if (issueType === IssueType.Bug && this.receivedSystemInfo) { + return true; + } + + if (issueType === IssueType.PerformanceIssue && this.receivedSystemInfo && this.receivedPerformanceInfo) { + return true; + } + } + + return false; + } + + private getExtensionRepositoryUrl(): string | undefined { + const selectedExtension = this.issueReporterModel.getData().selectedExtension; + return selectedExtension && selectedExtension.repositoryUrl; + } + + public getExtensionBugsUrl(): string | undefined { + const selectedExtension = this.issueReporterModel.getData().selectedExtension; + return selectedExtension && selectedExtension.bugsUrl; + } + + public searchVSCodeIssues(title: string, issueDescription?: string): void { + if (title) { + this.searchDuplicates(title, issueDescription); + } else { + this.clearSearchResults(); + } + } + + public searchIssues(title: string, fileOnExtension: boolean | undefined, fileOnMarketplace: boolean | undefined): void { + if (fileOnExtension) { + return this.searchExtensionIssues(title); + } + + if (fileOnMarketplace) { + return this.searchMarketplaceIssues(title); + } + + const description = this.issueReporterModel.getData().issueDescription; + this.searchVSCodeIssues(title, description); + } + + private searchExtensionIssues(title: string): void { + const url = this.getExtensionGitHubUrl(); + if (title) { + const matches = /^https?:\/\/github\.com\/(.*)/.exec(url); + if (matches && matches.length) { + const repo = matches[1]; + return this.searchGitHub(repo, title); + } + + // If the extension has no repository, display empty search results + if (this.issueReporterModel.getData().selectedExtension) { + this.clearSearchResults(); + return this.displaySearchResults([]); + + } + } + + this.clearSearchResults(); + } + + private searchMarketplaceIssues(title: string): void { + if (title) { + const gitHubInfo = this.parseGitHubUrl(this.product.reportMarketplaceIssueUrl!); + if (gitHubInfo) { + return this.searchGitHub(`${gitHubInfo.owner}/${gitHubInfo.repositoryName}`, title); + } + } + } + + public async close(): Promise { + await this.issueMainService.$closeReporter(); + } + + public clearSearchResults(): void { + const similarIssues = this.getElementById('similar-issues')!; + similarIssues.innerText = ''; + this.numberOfSearchResultsDisplayed = 0; + } + + @debounce(300) + private searchGitHub(repo: string, title: string): void { + const query = `is:issue+repo:${repo}+${title}`; + const similarIssues = this.getElementById('similar-issues')!; + + fetch(`https://api.github.com/search/issues?q=${query}`).then((response) => { + response.json().then(result => { + similarIssues.innerText = ''; + if (result && result.items) { + this.displaySearchResults(result.items); + } else { + // If the items property isn't present, the rate limit has been hit + const message = $('div.list-title'); + message.textContent = localize('rateLimited', "GitHub query limit exceeded. Please wait."); + similarIssues.appendChild(message); + + const resetTime = response.headers.get('X-RateLimit-Reset'); + const timeToWait = resetTime ? parseInt(resetTime) - Math.floor(Date.now() / 1000) : 1; + if (this.shouldQueueSearch) { + this.shouldQueueSearch = false; + setTimeout(() => { + this.searchGitHub(repo, title); + this.shouldQueueSearch = true; + }, timeToWait * 1000); + } + } + }).catch(_ => { + console.warn('Timeout or query limit exceeded'); + }); + }).catch(_ => { + console.warn('Error fetching GitHub issues'); + }); + } + + @debounce(300) + private searchDuplicates(title: string, body?: string): void { + const url = 'https://vscode-probot.westus.cloudapp.azure.com:7890/duplicate_candidates'; + const init = { + method: 'POST', + body: JSON.stringify({ + title, + body + }), + headers: new Headers({ + 'Content-Type': 'application/json' + }) + }; + + fetch(url, init).then((response) => { + response.json().then(result => { + this.clearSearchResults(); + + if (result && result.candidates) { + this.displaySearchResults(result.candidates); + } else { + throw new Error('Unexpected response, no candidates property'); + } + }).catch(_ => { + // Ignore + }); + }).catch(_ => { + // Ignore + }); + } + + private displaySearchResults(results: SearchResult[]) { + const similarIssues = this.getElementById('similar-issues')!; + if (results.length) { + const issues = $('div.issues-container'); + const issuesText = $('div.list-title'); + issuesText.textContent = localize('similarIssues', "Similar issues"); + + this.numberOfSearchResultsDisplayed = results.length < 5 ? results.length : 5; + for (let i = 0; i < this.numberOfSearchResultsDisplayed; i++) { + const issue = results[i]; + const link = $('a.issue-link', { href: issue.html_url }); + link.textContent = issue.title; + link.title = issue.title; + link.addEventListener('click', (e) => this.openLink(e)); + link.addEventListener('auxclick', (e) => this.openLink(e)); + + let issueState: HTMLElement; + let item: HTMLElement; + if (issue.state) { + issueState = $('span.issue-state'); + + const issueIcon = $('span.issue-icon'); + issueIcon.appendChild(renderIcon(issue.state === 'open' ? Codicon.issueOpened : Codicon.issueClosed)); + + const issueStateLabel = $('span.issue-state.label'); + issueStateLabel.textContent = issue.state === 'open' ? localize('open', "Open") : localize('closed', "Closed"); + + issueState.title = issue.state === 'open' ? localize('open', "Open") : localize('closed', "Closed"); + issueState.appendChild(issueIcon); + issueState.appendChild(issueStateLabel); + + item = $('div.issue', undefined, issueState, link); + } else { + item = $('div.issue', undefined, link); + } + + issues.appendChild(item); + } + + similarIssues.appendChild(issuesText); + similarIssues.appendChild(issues); + } else { + const message = $('div.list-title'); + message.textContent = localize('noSimilarIssues', "No similar issues found"); + similarIssues.appendChild(message); + } + } + + private setUpTypes(): void { + const makeOption = (issueType: IssueType, description: string) => $('option', { 'value': issueType.valueOf() }, escape(description)); + + const typeSelect = this.getElementById('issue-type')! as HTMLSelectElement; + const { issueType } = this.issueReporterModel.getData(); + reset(typeSelect, + makeOption(IssueType.Bug, localize('bugReporter', "Bug Report")), + makeOption(IssueType.FeatureRequest, localize('featureRequest', "Feature Request")), + makeOption(IssueType.PerformanceIssue, localize('performanceIssue', "Performance Issue (freeze, slow, crash)")) + ); + + typeSelect.value = issueType.toString(); + + this.setSourceOptions(); + } + + public makeOption(value: string, description: string, disabled: boolean): HTMLOptionElement { + const option: HTMLOptionElement = document.createElement('option'); + option.disabled = disabled; + option.value = value; + option.textContent = description; + + return option; + } + + public setSourceOptions(): void { + const sourceSelect = this.getElementById('issue-source')! as HTMLSelectElement; + const { issueType, fileOnExtension, selectedExtension, fileOnMarketplace, fileOnProduct } = this.issueReporterModel.getData(); + let selected = sourceSelect.selectedIndex; + if (selected === -1) { + if (fileOnExtension !== undefined) { + selected = fileOnExtension ? 2 : 1; + } else if (selectedExtension?.isBuiltin) { + selected = 1; + } else if (fileOnMarketplace) { + selected = 3; + } else if (fileOnProduct) { + selected = 1; + } + } + + sourceSelect.innerText = ''; + sourceSelect.append(this.makeOption('', localize('selectSource', "Select source"), true)); + sourceSelect.append(this.makeOption(IssueSource.VSCode, localize('vscode', "Visual Studio Code"), false)); + sourceSelect.append(this.makeOption(IssueSource.Extension, localize('extension', "A VS Code extension"), false)); + if (this.product.reportMarketplaceIssueUrl) { + sourceSelect.append(this.makeOption(IssueSource.Marketplace, localize('marketplace', "Extensions Marketplace"), false)); + } + + if (issueType !== IssueType.FeatureRequest) { + sourceSelect.append(this.makeOption(IssueSource.Unknown, localize('unknown', "Don't know"), false)); + } + + if (selected !== -1 && selected < sourceSelect.options.length) { + sourceSelect.selectedIndex = selected; + } else { + sourceSelect.selectedIndex = 0; + hide(this.getElementById('problem-source-help-text')); + } + } + + public renderBlocks(): void { + // Depending on Issue Type, we render different blocks and text + const { issueType, fileOnExtension, fileOnMarketplace, selectedExtension } = this.issueReporterModel.getData(); + const blockContainer = this.getElementById('block-container'); + const systemBlock = this.window.document.querySelector('.block-system'); + const processBlock = this.window.document.querySelector('.block-process'); + const workspaceBlock = this.window.document.querySelector('.block-workspace'); + const extensionsBlock = this.window.document.querySelector('.block-extensions'); + const experimentsBlock = this.window.document.querySelector('.block-experiments'); + const extensionDataBlock = this.window.document.querySelector('.block-extension-data'); + + const problemSource = this.getElementById('problem-source')!; + const descriptionTitle = this.getElementById('issue-description-label')!; + const descriptionSubtitle = this.getElementById('issue-description-subtitle')!; + const extensionSelector = this.getElementById('extension-selection')!; + + const titleTextArea = this.getElementById('issue-title-container')!; + const descriptionTextArea = this.getElementById('description')!; + const extensionDataTextArea = this.getElementById('extension-data')!; + + // Hide all by default + hide(blockContainer); + hide(systemBlock); + hide(processBlock); + hide(workspaceBlock); + hide(extensionsBlock); + hide(experimentsBlock); + hide(extensionSelector); + hide(extensionDataTextArea); + hide(extensionDataBlock); + + show(problemSource); + show(titleTextArea); + show(descriptionTextArea); + + if (fileOnExtension) { + show(extensionSelector); + } + + + if (selectedExtension && this.nonGitHubIssueUrl) { + hide(titleTextArea); + hide(descriptionTextArea); + reset(descriptionTitle, localize('handlesIssuesElsewhere', "This extension handles issues outside of VS Code")); + reset(descriptionSubtitle, localize('elsewhereDescription', "The '{0}' extension prefers to use an external issue reporter. To be taken to that issue reporting experience, click the button below.", selectedExtension.displayName)); + this.previewButton.label = localize('openIssueReporter', "Open External Issue Reporter"); + return; + } + + if (fileOnExtension && selectedExtension?.data) { + const data = selectedExtension?.data; + (extensionDataTextArea as HTMLElement).innerText = data.toString(); + (extensionDataTextArea as HTMLTextAreaElement).readOnly = true; + show(extensionDataBlock); + } + + // only if we know comes from the open reporter command + if (fileOnExtension && this.openReporter) { + (extensionDataTextArea as HTMLTextAreaElement).readOnly = true; + setTimeout(() => { + // delay to make sure from command or not + if (this.openReporter) { + show(extensionDataBlock); + } + }, 100); + show(extensionDataBlock); + } + + if (issueType === IssueType.Bug) { + if (!fileOnMarketplace) { + show(blockContainer); + show(systemBlock); + show(experimentsBlock); + if (!fileOnExtension) { + show(extensionsBlock); + } + } + + reset(descriptionTitle, localize('stepsToReproduce', "Steps to Reproduce") + ' ', $('span.required-input', undefined, '*')); + reset(descriptionSubtitle, localize('bugDescription', "Share the steps needed to reliably reproduce the problem. Please include actual and expected results. We support GitHub-flavored Markdown. You will be able to edit your issue and add screenshots when we preview it on GitHub.")); + } else if (issueType === IssueType.PerformanceIssue) { + if (!fileOnMarketplace) { + show(blockContainer); + show(systemBlock); + show(processBlock); + show(workspaceBlock); + show(experimentsBlock); + } + + if (fileOnExtension) { + show(extensionSelector); + } else if (!fileOnMarketplace) { + show(extensionsBlock); + } + + reset(descriptionTitle, localize('stepsToReproduce', "Steps to Reproduce") + ' ', $('span.required-input', undefined, '*')); + reset(descriptionSubtitle, localize('performanceIssueDesciption', "When did this performance issue happen? Does it occur on startup or after a specific series of actions? We support GitHub-flavored Markdown. You will be able to edit your issue and add screenshots when we preview it on GitHub.")); + } else if (issueType === IssueType.FeatureRequest) { + reset(descriptionTitle, localize('description', "Description") + ' ', $('span.required-input', undefined, '*')); + reset(descriptionSubtitle, localize('featureRequestDescription', "Please describe the feature you would like to see. We support GitHub-flavored Markdown. You will be able to edit your issue and add screenshots when we preview it on GitHub.")); + } + } + + public validateInput(inputId: string): boolean { + const inputElement = (this.getElementById(inputId)); + const inputValidationMessage = this.getElementById(`${inputId}-empty-error`); + const descriptionShortMessage = this.getElementById(`description-short-error`); + if (!inputElement.value) { + inputElement.classList.add('invalid-input'); + inputValidationMessage?.classList.remove('hidden'); + descriptionShortMessage?.classList.add('hidden'); + return false; + } else if (inputId === 'description' && inputElement.value.length < 10) { + inputElement.classList.add('invalid-input'); + descriptionShortMessage?.classList.remove('hidden'); + inputValidationMessage?.classList.add('hidden'); + return false; + } + else { + inputElement.classList.remove('invalid-input'); + inputValidationMessage?.classList.add('hidden'); + if (inputId === 'description') { + descriptionShortMessage?.classList.add('hidden'); + } + return true; + } + } + + public validateInputs(): boolean { + let isValid = true; + ['issue-title', 'description', 'issue-source'].forEach(elementId => { + isValid = this.validateInput(elementId) && isValid; + }); + + if (this.issueReporterModel.fileOnExtension()) { + isValid = this.validateInput('extension-selector') && isValid; + } + + return isValid; + } + + public async submitToGitHub(issueTitle: string, issueBody: string, gitHubDetails: { owner: string; repositoryName: string }): Promise { + const url = `https://api.github.com/repos/${gitHubDetails.owner}/${gitHubDetails.repositoryName}/issues`; + const init = { + method: 'POST', + body: JSON.stringify({ + title: issueTitle, + body: issueBody + }), + headers: new Headers({ + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${this.data.githubAccessToken}` + }) + }; + + const response = await fetch(url, init); + if (!response.ok) { + console.error('Invalid GitHub URL provided.'); + return false; + } + const result = await response.json(); + // if (this.nativeHostService) { + // await this.nativeHostService.openExternal(result.html_url); + // } + + this.window.open(result.html_url, '_blank'); + + this.close(); + return true; + } + + public async createIssue(): Promise { + const selectedExtension = this.issueReporterModel.getData().selectedExtension; + const hasUri = this.nonGitHubIssueUrl; + // Short circuit if the extension provides a custom issue handler + if (hasUri) { + const url = this.getExtensionBugsUrl(); + if (url) { + this.hasBeenSubmitted = true; + // if (this.nativeHostService) { + // await this.nativeHostService.openExternal(url); + // } + + return true; + } + } + + if (!this.validateInputs()) { + // If inputs are invalid, set focus to the first one and add listeners on them + // to detect further changes + const invalidInput = this.window.document.getElementsByClassName('invalid-input'); + if (invalidInput.length) { + (invalidInput[0]).focus(); + } + + this.addEventListener('issue-title', 'input', _ => { + this.validateInput('issue-title'); + }); + + this.addEventListener('description', 'input', _ => { + this.validateInput('description'); + }); + + this.addEventListener('issue-source', 'change', _ => { + this.validateInput('issue-source'); + }); + + if (this.issueReporterModel.fileOnExtension()) { + this.addEventListener('extension-selector', 'change', _ => { + this.validateInput('extension-selector'); + }); + } + + return false; + } + + this.hasBeenSubmitted = true; + + const issueTitle = (this.getElementById('issue-title')).value; + const issueBody = this.issueReporterModel.serialize(); + + let issueUrl = this.getIssueUrl(); + if (!issueUrl) { + console.error('No issue url found'); + return false; + } + + if (selectedExtension?.uri) { + const uri = URI.revive(selectedExtension.uri); + issueUrl = uri.toString(); + } + + const gitHubDetails = this.parseGitHubUrl(issueUrl); + if (this.data.githubAccessToken && gitHubDetails) { + return this.submitToGitHub(issueTitle, issueBody, gitHubDetails); + } + + const baseUrl = this.getIssueUrlWithTitle((this.getElementById('issue-title')).value, issueUrl); + let url = baseUrl + `&body=${encodeURIComponent(issueBody)}`; + + if (url.length > MAX_URL_LENGTH) { + try { + url = await this.writeToClipboard(baseUrl, issueBody); + } catch (_) { + console.error('Writing to clipboard failed'); + return false; + } + } + + this.window.open(url, '_blank'); + + return true; + } + + public async writeToClipboard(baseUrl: string, issueBody: string): Promise { + const shouldWrite = await this.issueMainService.$showClipboardDialog(); + if (!shouldWrite) { + throw new CancellationError(); + } + + return baseUrl + `&body=${encodeURIComponent(localize('pasteData', "We have written the needed data into your clipboard because it was too large to send. Please paste."))}`; + } + + public getIssueUrl(): string { + return this.issueReporterModel.fileOnExtension() + ? this.getExtensionGitHubUrl() + : this.issueReporterModel.getData().fileOnMarketplace + ? this.product.reportMarketplaceIssueUrl! + : this.product.reportIssueUrl!; + } + + public parseGitHubUrl(url: string): undefined | { repositoryName: string; owner: string } { + // Assumes a GitHub url to a particular repo, https://github.com/repositoryName/owner. + // Repository name and owner cannot contain '/' + const match = /^https?:\/\/github\.com\/([^\/]*)\/([^\/]*).*/.exec(url); + if (match && match.length) { + return { + owner: match[1], + repositoryName: match[2] + }; + } else { + console.error('No GitHub issues match'); + } + + return undefined; + } + + private getExtensionGitHubUrl(): string { + let repositoryUrl = ''; + const bugsUrl = this.getExtensionBugsUrl(); + const extensionUrl = this.getExtensionRepositoryUrl(); + // If given, try to match the extension's bug url + if (bugsUrl && bugsUrl.match(/^https?:\/\/github\.com\/([^\/]*)\/([^\/]*)\/?(\/issues)?$/)) { + // matches exactly: https://github.com/owner/repo/issues + repositoryUrl = normalizeGitHubUrl(bugsUrl); + } else if (extensionUrl && extensionUrl.match(/^https?:\/\/github\.com\/([^\/]*)\/([^\/]*)$/)) { + // matches exactly: https://github.com/owner/repo + repositoryUrl = normalizeGitHubUrl(extensionUrl); + } else { + this.nonGitHubIssueUrl = true; + repositoryUrl = bugsUrl || extensionUrl || ''; + } + + return repositoryUrl; + } + + public getIssueUrlWithTitle(issueTitle: string, repositoryUrl: string): string { + if (this.issueReporterModel.fileOnExtension()) { + repositoryUrl = repositoryUrl + '/issues/new'; + } + + const queryStringPrefix = repositoryUrl.indexOf('?') === -1 ? '?' : '&'; + return `${repositoryUrl}${queryStringPrefix}title=${encodeURIComponent(issueTitle)}`; + } + + public clearExtensionData(): void { + this.nonGitHubIssueUrl = false; + this.issueReporterModel.update({ extensionData: undefined }); + this.data.issueBody = undefined; + this.data.data = undefined; + this.data.uri = undefined; + } + + public async updateExtensionStatus(extension: IssueReporterExtensionData) { + this.issueReporterModel.update({ selectedExtension: extension }); + + // uses this.configuuration.data to ensure that data is coming from `openReporter` command. + const template = this.data.issueBody; + if (template) { + const descriptionTextArea = this.getElementById('description')!; + const descriptionText = (descriptionTextArea as HTMLTextAreaElement).value; + if (descriptionText === '' || !descriptionText.includes(template.toString())) { + const fullTextArea = descriptionText + (descriptionText === '' ? '' : '\n') + template.toString(); + (descriptionTextArea as HTMLTextAreaElement).value = fullTextArea; + this.issueReporterModel.update({ issueDescription: fullTextArea }); + } + } + + const data = this.data.data; + if (data) { + this.issueReporterModel.update({ extensionData: data }); + extension.data = data; + const extensionDataBlock = this.window.document.querySelector('.block-extension-data')!; + show(extensionDataBlock); + this.renderBlocks(); + } + + const uri = this.data.uri; + if (uri) { + extension.uri = uri; + this.updateIssueReporterUri(extension); + } + + this.validateSelectedExtension(); + const title = (this.getElementById('issue-title')).value; + this.searchExtensionIssues(title); + + this.updatePreviewButtonState(); + this.renderBlocks(); + } + + public validateSelectedExtension(): void { + const extensionValidationMessage = this.getElementById('extension-selection-validation-error')!; + const extensionValidationNoUrlsMessage = this.getElementById('extension-selection-validation-error-no-url')!; + hide(extensionValidationMessage); + hide(extensionValidationNoUrlsMessage); + + const extension = this.issueReporterModel.getData().selectedExtension; + if (!extension) { + this.previewButton.enabled = true; + return; + } + + if (this.loadingExtensionData) { + return; + } + + const hasValidGitHubUrl = this.getExtensionGitHubUrl(); + if (hasValidGitHubUrl) { + this.previewButton.enabled = true; + } else { + this.setExtensionValidationMessage(); + this.previewButton.enabled = false; + } + } + + public setLoading(element: HTMLElement) { + // Show loading + this.openReporter = true; + this.loadingExtensionData = true; + this.updatePreviewButtonState(); + + const extensionDataCaption = this.getElementById('extension-id')!; + hide(extensionDataCaption); + + const extensionDataCaption2 = Array.from(this.window.document.querySelectorAll('.ext-parens')); + extensionDataCaption2.forEach(extensionDataCaption2 => hide(extensionDataCaption2)); + + const showLoading = this.getElementById('ext-loading')!; + show(showLoading); + while (showLoading.firstChild) { + showLoading.removeChild(showLoading.firstChild); + } + showLoading.append(element); + + this.renderBlocks(); + } + + public removeLoading(element: HTMLElement, fromReporter: boolean = false) { + this.openReporter = fromReporter; + this.loadingExtensionData = false; + this.updatePreviewButtonState(); + + const extensionDataCaption = this.getElementById('extension-id')!; + show(extensionDataCaption); + + const extensionDataCaption2 = Array.from(this.window.document.querySelectorAll('.ext-parens')); + extensionDataCaption2.forEach(extensionDataCaption2 => show(extensionDataCaption2)); + + const hideLoading = this.getElementById('ext-loading')!; + hide(hideLoading); + if (hideLoading.firstChild) { + hideLoading.removeChild(element); + } + this.renderBlocks(); + } + + private setExtensionValidationMessage(): void { + const extensionValidationMessage = this.getElementById('extension-selection-validation-error')!; + const extensionValidationNoUrlsMessage = this.getElementById('extension-selection-validation-error-no-url')!; + const bugsUrl = this.getExtensionBugsUrl(); + if (bugsUrl) { + show(extensionValidationMessage); + const link = this.getElementById('extensionBugsLink')!; + link.textContent = bugsUrl; + return; + } + + const extensionUrl = this.getExtensionRepositoryUrl(); + if (extensionUrl) { + show(extensionValidationMessage); + const link = this.getElementById('extensionBugsLink'); + link!.textContent = extensionUrl; + return; + } + + show(extensionValidationNoUrlsMessage); + } + + private updateProcessInfo(state: IssueReporterModelData) { + const target = this.window.document.querySelector('.block-process .block-info') as HTMLElement; + if (target) { + reset(target, $('code', undefined, state.processInfo ?? '')); + } + } + + private updateWorkspaceInfo(state: IssueReporterModelData) { + this.window.document.querySelector('.block-workspace .block-info code')!.textContent = '\n' + state.workspaceInfo; + } + + public updateExtensionTable(extensions: IssueReporterExtensionData[], numThemeExtensions: number): void { + const target = this.window.document.querySelector('.block-extensions .block-info'); + if (target) { + if (this.disableExtensions) { + reset(target, localize('disabledExtensions', "Extensions are disabled")); + return; + } + + const themeExclusionStr = numThemeExtensions ? `\n(${numThemeExtensions} theme extensions excluded)` : ''; + extensions = extensions || []; + + if (!extensions.length) { + target.innerText = 'Extensions: none' + themeExclusionStr; + return; + } + + reset(target, this.getExtensionTableHtml(extensions), document.createTextNode(themeExclusionStr)); + } + } + + private getExtensionTableHtml(extensions: IssueReporterExtensionData[]): HTMLTableElement { + return $('table', undefined, + $('tr', undefined, + $('th', undefined, 'Extension'), + $('th', undefined, 'Author (truncated)' as string), + $('th', undefined, 'Version') + ), + ...extensions.map(extension => $('tr', undefined, + $('td', undefined, extension.name), + $('td', undefined, extension.publisher?.substr(0, 3) ?? 'N/A'), + $('td', undefined, extension.version) + )) + ); + } + + private openLink(event: MouseEvent): void { + event.preventDefault(); + event.stopPropagation(); + // Exclude right click + if (event.which < 3) { + windowOpenNoOpener((event.target).href); + } + } + + public getElementById(elementId: string): T | undefined { + const element = this.window.document.getElementById(elementId) as T | undefined; + if (element) { + return element; + } else { + return undefined; + } + } + + public addEventListener(elementId: string, eventType: string, handler: (event: Event) => void): void { + const element = this.getElementById(elementId); + element?.addEventListener(eventType, handler); + } +} + +// helper functions + +export function hide(el: Element | undefined | null) { + el?.classList.add('hidden'); +} +export function show(el: Element | undefined | null) { + el?.classList.remove('hidden'); +} + + diff --git a/src/vs/code/electron-sandbox/issue/issueReporterModel.ts b/src/vs/code/browser/issue/issueReporterModel.ts similarity index 100% rename from src/vs/code/electron-sandbox/issue/issueReporterModel.ts rename to src/vs/code/browser/issue/issueReporterModel.ts diff --git a/src/vs/code/electron-sandbox/issue/issueReporterPage.ts b/src/vs/code/browser/issue/issueReporterPage.ts similarity index 100% rename from src/vs/code/electron-sandbox/issue/issueReporterPage.ts rename to src/vs/code/browser/issue/issueReporterPage.ts diff --git a/src/vs/code/browser/issue/issueReporterService.ts b/src/vs/code/browser/issue/issueReporterService.ts new file mode 100644 index 00000000000..350a9057989 --- /dev/null +++ b/src/vs/code/browser/issue/issueReporterService.ts @@ -0,0 +1,203 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { $, reset, windowOpenNoOpener } from 'vs/base/browser/dom'; +import { groupBy } from 'vs/base/common/collections'; +import { isMacintosh } from 'vs/base/common/platform'; +import { IProductConfiguration } from 'vs/base/common/product'; +import { BaseIssueReporterService } from 'vs/code/browser/issue/issue'; +import { localize } from 'vs/nls'; +import { IIssueMainService, IssueReporterData, IssueReporterExtensionData } from 'vs/platform/issue/common/issue'; + +// GitHub has let us know that we could up our limit here to 8k. We chose 7500 to play it safe. +// ref https://github.com/microsoft/vscode/issues/159191 + +export class IssueWebReporter extends BaseIssueReporterService { + constructor( + disableExtensions: boolean, + data: IssueReporterData, + os: { + type: string; + arch: string; + release: string; + }, + product: IProductConfiguration, + window: Window, + @IIssueMainService issueMainService: IIssueMainService + ) { + super(disableExtensions, data, os, product, window, true, issueMainService); + this.setEventHandlers(); + this.handleExtensionData(data.enabledExtensions); + } + + private handleExtensionData(extensions: IssueReporterExtensionData[]) { + const installedExtensions = extensions.filter(x => !x.isBuiltin); + const { nonThemes, themes } = groupBy(installedExtensions, ext => { + return ext.isTheme ? 'themes' : 'nonThemes'; + }); + + const numberOfThemeExtesions = themes && themes.length; + this.issueReporterModel.update({ numberOfThemeExtesions, enabledNonThemeExtesions: nonThemes, allExtensions: installedExtensions }); + this.updateExtensionTable(nonThemes, numberOfThemeExtesions); + if (this.disableExtensions || installedExtensions.length === 0) { + (this.getElementById('disableExtensions')).disabled = true; + } + + this.updateExtensionSelector(installedExtensions); + } + + private async sendReporterMenu(extension: IssueReporterExtensionData): Promise { + try { + const data = await this.issueMainService.$sendReporterMenu(extension.id, extension.name); + return data; + } catch (e) { + console.error(e); + return undefined; + } + } + + public override setEventHandlers(): void { + super.setEventHandlers(); + this.previewButton.onDidClick(async () => { + this.delayedSubmit.trigger(async () => { + this.createIssue(); + }); + }); + + this.addEventListener('disableExtensions', 'click', () => { + this.issueMainService.$reloadWithExtensionsDisabled(); + }); + + this.addEventListener('extensionBugsLink', 'click', (e: Event) => { + const url = (e.target).innerText; + windowOpenNoOpener(url); + }); + + this.addEventListener('disableExtensions', 'keydown', (e: Event) => { + e.stopPropagation(); + if ((e as KeyboardEvent).key === "Enter" || (e as KeyboardEvent).key === " ") { + this.issueMainService.$reloadWithExtensionsDisabled(); + } + }); + + this.window.document.onkeydown = async (e: KeyboardEvent) => { + const cmdOrCtrlKey = isMacintosh ? e.metaKey : e.ctrlKey; + // Cmd/Ctrl+Enter previews issue and closes window + if (cmdOrCtrlKey && e.key === "Enter") { + this.delayedSubmit.trigger(async () => { + if (await this.createIssue()) { + this.close(); + } + }); + } + + // Cmd/Ctrl + w closes issue window + if (cmdOrCtrlKey && e.key === "w") { + e.stopPropagation(); + e.preventDefault(); + + const issueTitle = (this.getElementById('issue-title'))!.value; + const { issueDescription } = this.issueReporterModel.getData(); + if (!this.hasBeenSubmitted && (issueTitle || issueDescription)) { + // fire and forget + this.issueMainService.$showConfirmCloseDialog(); + } else { + this.close(); + } + } + + // Cmd/Ctrl + zooms in + if (cmdOrCtrlKey && e.key === "+") { + // zoomIn(this.window); + } + + // Cmd/Ctrl - zooms out + if (cmdOrCtrlKey && e.key === "-") { + // zoomOut(this.window); + } + + // With latest electron upgrade, cmd+a is no longer propagating correctly for inputs in this window on mac + // Manually perform the selection + if (isMacintosh) { + if (cmdOrCtrlKey && e.key === "a" && e.target) { + if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) { + (e.target).select(); + } + } + } + }; + } + + private updateExtensionSelector(extensions: IssueReporterExtensionData[]): void { + interface IOption { + name: string; + id: string; + } + + const extensionOptions: IOption[] = extensions.map(extension => { + return { + name: extension.displayName || extension.name || '', + id: extension.id + }; + }); + + // Sort extensions by name + extensionOptions.sort((a, b) => { + const aName = a.name.toLowerCase(); + const bName = b.name.toLowerCase(); + if (aName > bName) { + return 1; + } + + if (aName < bName) { + return -1; + } + + return 0; + }); + + const makeOption = (extension: IOption, selectedExtension?: IssueReporterExtensionData): HTMLOptionElement => { + const selected = selectedExtension && extension.id === selectedExtension.id; + return $('option', { + 'value': extension.id, + 'selected': selected || '' + }, extension.name); + }; + + const extensionsSelector = this.getElementById('extension-selector'); + if (extensionsSelector) { + const { selectedExtension } = this.issueReporterModel.getData(); + reset(extensionsSelector, this.makeOption('', localize('selectExtension', "Select extension"), true), ...extensionOptions.map(extension => makeOption(extension, selectedExtension))); + + if (!selectedExtension) { + extensionsSelector.selectedIndex = 0; + } + + this.addEventListener('extension-selector', 'change', async (e: Event) => { + this.clearExtensionData(); + const selectedExtensionId = (e.target).value; + this.selectedExtension = selectedExtensionId; + const extensions = this.issueReporterModel.getData().allExtensions; + const matches = extensions.filter(extension => extension.id === selectedExtensionId); + if (matches.length) { + this.issueReporterModel.update({ selectedExtension: matches[0] }); + const selectedExtension = this.issueReporterModel.getData().selectedExtension; + if (selectedExtension) { + await this.sendReporterMenu(selectedExtension); + } else { + this.issueReporterModel.update({ selectedExtension: undefined }); + this.clearSearchResults(); + this.clearExtensionData(); + this.validateSelectedExtension(); + this.updateExtensionStatus(matches[0]); + } + } + }); + } + + this.addEventListener('problem-source', 'change', (_) => { + this.validateSelectedExtension(); + }); + } +} diff --git a/src/vs/code/electron-sandbox/issue/issueReporterMain.ts b/src/vs/code/electron-sandbox/issue/issueReporterMain.ts index 2cbe4b0f8d2..c72367bff1a 100644 --- a/src/vs/code/electron-sandbox/issue/issueReporterMain.ts +++ b/src/vs/code/electron-sandbox/issue/issueReporterMain.ts @@ -6,7 +6,7 @@ import { safeInnerHtml } from 'vs/base/browser/dom'; import 'vs/base/browser/ui/codicons/codiconStyles'; // make sure codicon css is loaded import { isLinux, isWindows } from 'vs/base/common/platform'; -import BaseHtml from 'vs/code/electron-sandbox/issue/issueReporterPage'; +import BaseHtml from 'vs/code/browser/issue/issueReporterPage'; import 'vs/css!./media/issueReporter'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { getSingletonServiceDescriptors } from 'vs/platform/instantiation/common/extensions'; @@ -18,7 +18,7 @@ import { registerMainProcessRemoteService } from 'vs/platform/ipc/electron-sandb import { IIssueMainService, IssueReporterWindowConfiguration } from 'vs/platform/issue/common/issue'; import { INativeHostService } from 'vs/platform/native/common/native'; import { NativeHostService } from 'vs/platform/native/common/nativeHostService'; -import { IssueReporter } from './issueReporterService'; +import { IssueReporter2 } from 'vs/code/electron-sandbox/issue/issueReporterService2'; import { mainWindow } from 'vs/base/browser/window'; export function startup(configuration: IssueReporterWindowConfiguration) { @@ -29,7 +29,7 @@ export function startup(configuration: IssueReporterWindowConfiguration) { const instantiationService = initServices(configuration.windowId); - const issueReporter = instantiationService.createInstance(IssueReporter, configuration); + const issueReporter = instantiationService.createInstance(IssueReporter2, configuration); issueReporter.render(); mainWindow.document.body.style.display = 'block'; issueReporter.setInitialFocus(); diff --git a/src/vs/code/electron-sandbox/issue/issueReporterService.ts b/src/vs/code/electron-sandbox/issue/issueReporterService.ts index 6edb382d5ac..a295a881487 100644 --- a/src/vs/code/electron-sandbox/issue/issueReporterService.ts +++ b/src/vs/code/electron-sandbox/issue/issueReporterService.ts @@ -16,7 +16,7 @@ import { isLinuxSnap, isMacintosh } from 'vs/base/common/platform'; import { escape } from 'vs/base/common/strings'; import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; -import { IssueReporterModel, IssueReporterData as IssueReporterModelData } from 'vs/code/electron-sandbox/issue/issueReporterModel'; +import { IssueReporterModel, IssueReporterData as IssueReporterModelData } from 'vs/code/browser/issue/issueReporterModel'; import { localize } from 'vs/nls'; import { isRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnostics'; import { IIssueMainService, IssueReporterData, IssueReporterExtensionData, IssueReporterStyles, IssueReporterWindowConfiguration, IssueType } from 'vs/platform/issue/common/issue'; diff --git a/src/vs/code/electron-sandbox/issue/issueReporterService2.ts b/src/vs/code/electron-sandbox/issue/issueReporterService2.ts new file mode 100644 index 00000000000..ac029e9909c --- /dev/null +++ b/src/vs/code/electron-sandbox/issue/issueReporterService2.ts @@ -0,0 +1,508 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { $, reset, windowOpenNoOpener } from 'vs/base/browser/dom'; +import { mainWindow } from 'vs/base/browser/window'; +import { Codicon } from 'vs/base/common/codicons'; +import { groupBy } from 'vs/base/common/collections'; +import { CancellationError } from 'vs/base/common/errors'; +import { isMacintosh } from 'vs/base/common/platform'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { URI } from 'vs/base/common/uri'; +import { IssueReporterData as IssueReporterModelData } from 'vs/code/browser/issue/issueReporterModel'; +import { BaseIssueReporterService, hide, show } from 'vs/code/browser/issue/issue'; +import { localize } from 'vs/nls'; +import { isRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnostics'; +import { IIssueMainService, IssueReporterData, IssueReporterExtensionData, IssueReporterWindowConfiguration, IssueType } from 'vs/platform/issue/common/issue'; +import { INativeHostService } from 'vs/platform/native/common/native'; +import { applyZoom, zoomIn, zoomOut } from 'vs/platform/window/electron-sandbox/window'; + +// GitHub has let us know that we could up our limit here to 8k. We chose 7500 to play it safe. +// ref https://github.com/microsoft/vscode/issues/159191 +const MAX_URL_LENGTH = 7500; + + +export class IssueReporter2 extends BaseIssueReporterService { + constructor( + private readonly configuration: IssueReporterWindowConfiguration, + @INativeHostService private readonly nativeHostService: INativeHostService, + @IIssueMainService issueMainService: IIssueMainService + ) { + super(configuration.disableExtensions, configuration.data, configuration.os, configuration.product, mainWindow, false, issueMainService); + + this.issueMainService.$getSystemInfo().then(info => { + this.issueReporterModel.update({ systemInfo: info }); + this.receivedSystemInfo = true; + + this.updateSystemInfo(this.issueReporterModel.getData()); + this.updatePreviewButtonState(); + }); + if (configuration.data.issueType === IssueType.PerformanceIssue) { + this.issueMainService.$getPerformanceInfo().then(info => { + this.updatePerformanceInfo(info as Partial); + }); + } + + this.setEventHandlers(); + applyZoom(configuration.data.zoomLevel, mainWindow); + this.handleExtensionData(configuration.data.enabledExtensions); + this.updateExperimentsInfo(configuration.data.experiments); + this.updateRestrictedMode(configuration.data.restrictedMode); + this.updateUnsupportedMode(configuration.data.isUnsupported); + } + + private handleExtensionData(extensions: IssueReporterExtensionData[]) { + const installedExtensions = extensions.filter(x => !x.isBuiltin); + const { nonThemes, themes } = groupBy(installedExtensions, ext => { + return ext.isTheme ? 'themes' : 'nonThemes'; + }); + + const numberOfThemeExtesions = themes && themes.length; + this.issueReporterModel.update({ numberOfThemeExtesions, enabledNonThemeExtesions: nonThemes, allExtensions: installedExtensions }); + this.updateExtensionTable(nonThemes, numberOfThemeExtesions); + if (this.disableExtensions || installedExtensions.length === 0) { + (this.getElementById('disableExtensions')).disabled = true; + } + + this.updateExtensionSelector(installedExtensions); + } + + private async sendReporterMenu(extension: IssueReporterExtensionData): Promise { + try { + const data = await this.issueMainService.$sendReporterMenu(extension.id, extension.name); + return data; + } catch (e) { + console.error(e); + return undefined; + } + } + + public override setEventHandlers(): void { + super.setEventHandlers(); + + // Keep all event listerns involving window and issue creation + this.previewButton.onDidClick(async () => { + this.delayedSubmit.trigger(async () => { + this.createIssue(); + }); + }); + + this.addEventListener('disableExtensions', 'click', () => { + this.issueMainService.$reloadWithExtensionsDisabled(); + }); + + this.addEventListener('extensionBugsLink', 'click', (e: Event) => { + const url = (e.target).innerText; + windowOpenNoOpener(url); + }); + + this.addEventListener('disableExtensions', 'keydown', (e: Event) => { + e.stopPropagation(); + if ((e as KeyboardEvent).keyCode === 13 || (e as KeyboardEvent).keyCode === 32) { + this.issueMainService.$reloadWithExtensionsDisabled(); + } + }); + + + // THIS IS THE MAIN IMPORTANT PART + mainWindow.document.onkeydown = async (e: KeyboardEvent) => { + const cmdOrCtrlKey = isMacintosh ? e.metaKey : e.ctrlKey; + // Cmd/Ctrl+Enter previews issue and closes window + if (cmdOrCtrlKey && e.key === "Enter") { + this.delayedSubmit.trigger(async () => { + if (await this.createIssue()) { + this.close(); + } + }); + } + + // Cmd/Ctrl + w closes issue window + if (cmdOrCtrlKey && e.key === "w") { + e.stopPropagation(); + e.preventDefault(); + + const issueTitle = (this.getElementById('issue-title'))!.value; + const { issueDescription } = this.issueReporterModel.getData(); + if (!this.hasBeenSubmitted && (issueTitle || issueDescription)) { + // fire and forget + this.issueMainService.$showConfirmCloseDialog(); + } else { + this.close(); + } + } + + // Cmd/Ctrl + zooms in + if (cmdOrCtrlKey && e.key === "+") { + zoomIn(mainWindow); + } + + // Cmd/Ctrl - zooms out + if (cmdOrCtrlKey && e.key === "-") { + zoomOut(mainWindow); + } + + // With latest electron upgrade, cmd+a is no longer propagating correctly for inputs in this window on mac + // Manually perform the selection + if (isMacintosh) { + if (cmdOrCtrlKey && e.key === "a" && e.target) { + if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) { + (e.target).select(); + } + } + } + }; + } + + public override async submitToGitHub(issueTitle: string, issueBody: string, gitHubDetails: { owner: string; repositoryName: string }): Promise { + const url = `https://api.github.com/repos/${gitHubDetails.owner}/${gitHubDetails.repositoryName}/issues`; + const init = { + method: 'POST', + body: JSON.stringify({ + title: issueTitle, + body: issueBody + }), + headers: new Headers({ + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${this.data.githubAccessToken}` + }) + }; + + const response = await fetch(url, init); + if (!response.ok) { + console.error('Invalid GitHub URL provided.'); + return false; + } + const result = await response.json(); + await this.nativeHostService.openExternal(result.html_url); + this.close(); + return true; + } + + public override async createIssue(): Promise { + const selectedExtension = this.issueReporterModel.getData().selectedExtension; + const hasUri = this.nonGitHubIssueUrl; + // Short circuit if the extension provides a custom issue handler + if (hasUri) { + const url = this.getExtensionBugsUrl(); + if (url) { + this.hasBeenSubmitted = true; + await this.nativeHostService.openExternal(url); + return true; + } + } + + if (!this.validateInputs()) { + // If inputs are invalid, set focus to the first one and add listeners on them + // to detect further changes + const invalidInput = mainWindow.document.getElementsByClassName('invalid-input'); + if (invalidInput.length) { + (invalidInput[0]).focus(); + } + + this.addEventListener('issue-title', 'input', _ => { + this.validateInput('issue-title'); + }); + + this.addEventListener('description', 'input', _ => { + this.validateInput('description'); + }); + + this.addEventListener('issue-source', 'change', _ => { + this.validateInput('issue-source'); + }); + + if (this.issueReporterModel.fileOnExtension()) { + this.addEventListener('extension-selector', 'change', _ => { + this.validateInput('extension-selector'); + }); + } + + return false; + } + + this.hasBeenSubmitted = true; + + const issueTitle = (this.getElementById('issue-title')).value; + const issueBody = this.issueReporterModel.serialize(); + + let issueUrl = this.getIssueUrl(); + if (!issueUrl) { + console.error('No issue url found'); + return false; + } + + if (selectedExtension?.uri) { + const uri = URI.revive(selectedExtension.uri); + issueUrl = uri.toString(); + } + + const gitHubDetails = this.parseGitHubUrl(issueUrl); + if (this.data.githubAccessToken && gitHubDetails) { + return this.submitToGitHub(issueTitle, issueBody, gitHubDetails); + } + + const baseUrl = this.getIssueUrlWithTitle((this.getElementById('issue-title')).value, issueUrl); + let url = baseUrl + `&body=${encodeURIComponent(issueBody)}`; + + if (url.length > MAX_URL_LENGTH) { + try { + url = await this.writeToClipboard(baseUrl, issueBody); + } catch (_) { + console.error('Writing to clipboard failed'); + return false; + } + } + + await this.nativeHostService.openExternal(url); + return true; + } + + public override async writeToClipboard(baseUrl: string, issueBody: string): Promise { + const shouldWrite = await this.issueMainService.$showClipboardDialog(); + if (!shouldWrite) { + throw new CancellationError(); + } + + await this.nativeHostService.writeClipboardText(issueBody); + + return baseUrl + `&body=${encodeURIComponent(localize('pasteData', "We have written the needed data into your clipboard because it was too large to send. Please paste."))}`; + } + + private updateSystemInfo(state: IssueReporterModelData) { + const target = mainWindow.document.querySelector('.block-system .block-info'); + + if (target) { + const systemInfo = state.systemInfo!; + const renderedDataTable = $('table', undefined, + $('tr', undefined, + $('td', undefined, 'CPUs'), + $('td', undefined, systemInfo.cpus || '') + ), + $('tr', undefined, + $('td', undefined, 'GPU Status' as string), + $('td', undefined, Object.keys(systemInfo.gpuStatus).map(key => `${key}: ${systemInfo.gpuStatus[key]}`).join('\n')) + ), + $('tr', undefined, + $('td', undefined, 'Load (avg)' as string), + $('td', undefined, systemInfo.load || '') + ), + $('tr', undefined, + $('td', undefined, 'Memory (System)' as string), + $('td', undefined, systemInfo.memory) + ), + $('tr', undefined, + $('td', undefined, 'Process Argv' as string), + $('td', undefined, systemInfo.processArgs) + ), + $('tr', undefined, + $('td', undefined, 'Screen Reader' as string), + $('td', undefined, systemInfo.screenReader) + ), + $('tr', undefined, + $('td', undefined, 'VM'), + $('td', undefined, systemInfo.vmHint) + ) + ); + reset(target, renderedDataTable); + + systemInfo.remoteData.forEach(remote => { + target.appendChild($('hr')); + if (isRemoteDiagnosticError(remote)) { + const remoteDataTable = $('table', undefined, + $('tr', undefined, + $('td', undefined, 'Remote'), + $('td', undefined, remote.hostName) + ), + $('tr', undefined, + $('td', undefined, ''), + $('td', undefined, remote.errorMessage) + ) + ); + target.appendChild(remoteDataTable); + } else { + const remoteDataTable = $('table', undefined, + $('tr', undefined, + $('td', undefined, 'Remote'), + $('td', undefined, remote.latency ? `${remote.hostName} (latency: ${remote.latency.current.toFixed(2)}ms last, ${remote.latency.average.toFixed(2)}ms average)` : remote.hostName) + ), + $('tr', undefined, + $('td', undefined, 'OS'), + $('td', undefined, remote.machineInfo.os) + ), + $('tr', undefined, + $('td', undefined, 'CPUs'), + $('td', undefined, remote.machineInfo.cpus || '') + ), + $('tr', undefined, + $('td', undefined, 'Memory (System)' as string), + $('td', undefined, remote.machineInfo.memory) + ), + $('tr', undefined, + $('td', undefined, 'VM'), + $('td', undefined, remote.machineInfo.vmHint) + ) + ); + target.appendChild(remoteDataTable); + } + }); + } + } + + public updateExtensionSelector(extensions: IssueReporterExtensionData[]): void { + interface IOption { + name: string; + id: string; + } + + const extensionOptions: IOption[] = extensions.map(extension => { + return { + name: extension.displayName || extension.name || '', + id: extension.id + }; + }); + + // Sort extensions by name + extensionOptions.sort((a, b) => { + const aName = a.name.toLowerCase(); + const bName = b.name.toLowerCase(); + if (aName > bName) { + return 1; + } + + if (aName < bName) { + return -1; + } + + return 0; + }); + + const makeOption = (extension: IOption, selectedExtension?: IssueReporterExtensionData): HTMLOptionElement => { + const selected = selectedExtension && extension.id === selectedExtension.id; + return $('option', { + 'value': extension.id, + 'selected': selected || '' + }, extension.name); + }; + + const extensionsSelector = this.getElementById('extension-selector'); + if (extensionsSelector) { + const { selectedExtension } = this.issueReporterModel.getData(); + reset(extensionsSelector, this.makeOption('', localize('selectExtension', "Select extension"), true), ...extensionOptions.map(extension => makeOption(extension, selectedExtension))); + + if (!selectedExtension) { + extensionsSelector.selectedIndex = 0; + } + + this.addEventListener('extension-selector', 'change', async (e: Event) => { + this.clearExtensionData(); + const selectedExtensionId = (e.target).value; + this.selectedExtension = selectedExtensionId; + const extensions = this.issueReporterModel.getData().allExtensions; + const matches = extensions.filter(extension => extension.id === selectedExtensionId); + if (matches.length) { + this.issueReporterModel.update({ selectedExtension: matches[0] }); + const selectedExtension = this.issueReporterModel.getData().selectedExtension; + if (selectedExtension) { + const iconElement = document.createElement('span'); + iconElement.classList.add(...ThemeIcon.asClassNameArray(Codicon.loading), 'codicon-modifier-spin'); + this.setLoading(iconElement); + const openReporterData = await this.sendReporterMenu(selectedExtension); + if (openReporterData) { + if (this.selectedExtension === selectedExtensionId) { + this.removeLoading(iconElement, true); + this.configuration.data = openReporterData; + this.data = openReporterData; + } else if (this.selectedExtension !== selectedExtensionId) { + } + } + else { + if (!this.loadingExtensionData) { + iconElement.classList.remove(...ThemeIcon.asClassNameArray(Codicon.loading), 'codicon-modifier-spin'); + } + this.removeLoading(iconElement); + // if not using command, should have no configuration data in fields we care about and check later. + this.clearExtensionData(); + + // case when previous extension was opened from normal openIssueReporter command + selectedExtension.data = undefined; + selectedExtension.uri = undefined; + } + if (this.selectedExtension === selectedExtensionId) { + // repopulates the fields with the new data given the selected extension. + this.updateExtensionStatus(matches[0]); + this.openReporter = false; + } + } else { + this.issueReporterModel.update({ selectedExtension: undefined }); + this.clearSearchResults(); + this.clearExtensionData(); + this.validateSelectedExtension(); + this.updateExtensionStatus(matches[0]); + } + } + }); + } + + this.addEventListener('problem-source', 'change', (_) => { + this.validateSelectedExtension(); + }); + } + + public override setLoading(element: HTMLElement) { + // Show loading + this.openReporter = true; + this.loadingExtensionData = true; + this.updatePreviewButtonState(); + + const extensionDataCaption = this.getElementById('extension-id')!; + hide(extensionDataCaption); + + const extensionDataCaption2 = Array.from(mainWindow.document.querySelectorAll('.ext-parens')); + extensionDataCaption2.forEach(extensionDataCaption2 => hide(extensionDataCaption2)); + + const showLoading = this.getElementById('ext-loading')!; + show(showLoading); + while (showLoading.firstChild) { + showLoading.removeChild(showLoading.firstChild); + } + showLoading.append(element); + + this.renderBlocks(); + } + + public override removeLoading(element: HTMLElement, fromReporter: boolean = false) { + this.openReporter = fromReporter; + this.loadingExtensionData = false; + this.updatePreviewButtonState(); + + const extensionDataCaption = this.getElementById('extension-id')!; + show(extensionDataCaption); + + const extensionDataCaption2 = Array.from(mainWindow.document.querySelectorAll('.ext-parens')); + extensionDataCaption2.forEach(extensionDataCaption2 => show(extensionDataCaption2)); + + const hideLoading = this.getElementById('ext-loading')!; + hide(hideLoading); + if (hideLoading.firstChild) { + hideLoading.removeChild(element); + } + this.renderBlocks(); + } + + private updateRestrictedMode(restrictedMode: boolean) { + this.issueReporterModel.update({ restrictedMode }); + } + + private updateUnsupportedMode(isUnsupported: boolean) { + this.issueReporterModel.update({ isUnsupported }); + } + + private updateExperimentsInfo(experimentInfo: string | undefined) { + this.issueReporterModel.update({ experimentInfo }); + const target = mainWindow.document.querySelector('.block-experiments .block-info'); + if (target) { + target.textContent = experimentInfo ? experimentInfo : localize('noCurrentExperiments', "No current experiments."); + } + } +} diff --git a/src/vs/code/test/electron-sandbox/issue/testReporterModel.test.ts b/src/vs/code/test/electron-sandbox/issue/testReporterModel.test.ts index be9c47d5bc4..1708f3a07a1 100644 --- a/src/vs/code/test/electron-sandbox/issue/testReporterModel.test.ts +++ b/src/vs/code/test/electron-sandbox/issue/testReporterModel.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import { IssueReporterModel } from 'vs/code/electron-sandbox/issue/issueReporterModel'; +import { IssueReporterModel } from 'vs/code/browser/issue/issueReporterModel'; import { IssueType } from 'vs/platform/issue/common/issue'; import { normalizeGitHubUrl } from 'vs/platform/issue/common/issueReporterUtil'; From 723f85125adb7709279650c73c532c7f8887feca Mon Sep 17 00:00:00 2001 From: David Dossett Date: Thu, 16 May 2024 11:42:13 -0700 Subject: [PATCH 247/357] Match attachment pill border radius to main chat input --- src/vs/workbench/contrib/chat/browser/media/chat.css | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 6effa1626ea..5c4aef84471 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -495,7 +495,6 @@ .interactive-session .chat-attached-context { padding: 0 0 8px 0; - border-radius: 6px; display: flex; gap: 4px; flex-wrap: wrap; @@ -504,7 +503,7 @@ .interactive-session .chat-attached-context .chat-attached-context-attachment { padding: 2px; border: 1px solid var(--vscode-chat-requestBorder, var(--vscode-input-background, transparent)); - border-radius: 5px; + border-radius: 4px; height: 18px; } From 601ea43caf9a6d3d21d942077e15989aab52f950 Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Thu, 16 May 2024 14:56:28 -0700 Subject: [PATCH 248/357] issue reporter refactor cleanup (#212932) * remove comments and fix new key usages * cleanup --- src/vs/code/browser/issue/issue.ts | 8 -------- .../code/browser/issue/issueReporterService.ts | 18 ++++-------------- .../issue/issueReporterService2.ts | 10 +++++----- 3 files changed, 9 insertions(+), 27 deletions(-) diff --git a/src/vs/code/browser/issue/issue.ts b/src/vs/code/browser/issue/issue.ts index cfbf5c75b7f..2205e847134 100644 --- a/src/vs/code/browser/issue/issue.ts +++ b/src/vs/code/browser/issue/issue.ts @@ -835,10 +835,6 @@ export class BaseIssueReporterService extends Disposable { return false; } const result = await response.json(); - // if (this.nativeHostService) { - // await this.nativeHostService.openExternal(result.html_url); - // } - this.window.open(result.html_url, '_blank'); this.close(); @@ -853,10 +849,6 @@ export class BaseIssueReporterService extends Disposable { const url = this.getExtensionBugsUrl(); if (url) { this.hasBeenSubmitted = true; - // if (this.nativeHostService) { - // await this.nativeHostService.openExternal(url); - // } - return true; } } diff --git a/src/vs/code/browser/issue/issueReporterService.ts b/src/vs/code/browser/issue/issueReporterService.ts index 350a9057989..d724e028e94 100644 --- a/src/vs/code/browser/issue/issueReporterService.ts +++ b/src/vs/code/browser/issue/issueReporterService.ts @@ -76,7 +76,7 @@ export class IssueWebReporter extends BaseIssueReporterService { this.addEventListener('disableExtensions', 'keydown', (e: Event) => { e.stopPropagation(); - if ((e as KeyboardEvent).key === "Enter" || (e as KeyboardEvent).key === " ") { + if ((e as KeyboardEvent).key === 'Enter' || (e as KeyboardEvent).key === ' ') { this.issueMainService.$reloadWithExtensionsDisabled(); } }); @@ -84,7 +84,7 @@ export class IssueWebReporter extends BaseIssueReporterService { this.window.document.onkeydown = async (e: KeyboardEvent) => { const cmdOrCtrlKey = isMacintosh ? e.metaKey : e.ctrlKey; // Cmd/Ctrl+Enter previews issue and closes window - if (cmdOrCtrlKey && e.key === "Enter") { + if (cmdOrCtrlKey && e.key === 'Enter') { this.delayedSubmit.trigger(async () => { if (await this.createIssue()) { this.close(); @@ -93,7 +93,7 @@ export class IssueWebReporter extends BaseIssueReporterService { } // Cmd/Ctrl + w closes issue window - if (cmdOrCtrlKey && e.key === "w") { + if (cmdOrCtrlKey && e.key === 'w') { e.stopPropagation(); e.preventDefault(); @@ -107,20 +107,10 @@ export class IssueWebReporter extends BaseIssueReporterService { } } - // Cmd/Ctrl + zooms in - if (cmdOrCtrlKey && e.key === "+") { - // zoomIn(this.window); - } - - // Cmd/Ctrl - zooms out - if (cmdOrCtrlKey && e.key === "-") { - // zoomOut(this.window); - } - // With latest electron upgrade, cmd+a is no longer propagating correctly for inputs in this window on mac // Manually perform the selection if (isMacintosh) { - if (cmdOrCtrlKey && e.key === "a" && e.target) { + if (cmdOrCtrlKey && e.key === 'a' && e.target) { if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) { (e.target).select(); } diff --git a/src/vs/code/electron-sandbox/issue/issueReporterService2.ts b/src/vs/code/electron-sandbox/issue/issueReporterService2.ts index ac029e9909c..51bdfe923b8 100644 --- a/src/vs/code/electron-sandbox/issue/issueReporterService2.ts +++ b/src/vs/code/electron-sandbox/issue/issueReporterService2.ts @@ -109,7 +109,7 @@ export class IssueReporter2 extends BaseIssueReporterService { mainWindow.document.onkeydown = async (e: KeyboardEvent) => { const cmdOrCtrlKey = isMacintosh ? e.metaKey : e.ctrlKey; // Cmd/Ctrl+Enter previews issue and closes window - if (cmdOrCtrlKey && e.key === "Enter") { + if (cmdOrCtrlKey && e.key === 'Enter') { this.delayedSubmit.trigger(async () => { if (await this.createIssue()) { this.close(); @@ -118,7 +118,7 @@ export class IssueReporter2 extends BaseIssueReporterService { } // Cmd/Ctrl + w closes issue window - if (cmdOrCtrlKey && e.key === "w") { + if (cmdOrCtrlKey && e.key === 'w') { e.stopPropagation(); e.preventDefault(); @@ -133,19 +133,19 @@ export class IssueReporter2 extends BaseIssueReporterService { } // Cmd/Ctrl + zooms in - if (cmdOrCtrlKey && e.key === "+") { + if (cmdOrCtrlKey && (e.key === '+' || e.key === '=')) { zoomIn(mainWindow); } // Cmd/Ctrl - zooms out - if (cmdOrCtrlKey && e.key === "-") { + if (cmdOrCtrlKey && e.key === '-') { zoomOut(mainWindow); } // With latest electron upgrade, cmd+a is no longer propagating correctly for inputs in this window on mac // Manually perform the selection if (isMacintosh) { - if (cmdOrCtrlKey && e.key === "a" && e.target) { + if (cmdOrCtrlKey && e.key === 'a' && e.target) { if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) { (e.target).select(); } From 1d449873eebb60f48486485d094af273782a33a7 Mon Sep 17 00:00:00 2001 From: Michael Lively Date: Thu, 16 May 2024 15:26:19 -0700 Subject: [PATCH 249/357] Support for `notebook.format` Code Action (#212750) * initial support + remove some unnecessary early returns * notebook default formatter setting + skip formatting if format codeaction + restructuring * support default formatter for notebooks + provider holds extensionId * remove unreachable code, simplify default formatter setting --- src/vs/editor/common/languages.ts | 2 + .../api/browser/mainThreadLanguageFeatures.ts | 7 +- .../workbench/api/common/extHost.protocol.ts | 2 +- .../api/common/extHostLanguageFeatures.ts | 10 +- .../format/browser/formatActionsMultiple.ts | 2 +- .../saveParticipants/saveParticipants.ts | 280 ++++++++++++------ .../notebook/browser/notebook.contribution.ts | 9 + .../contrib/notebook/common/notebookCommon.ts | 1 + 8 files changed, 219 insertions(+), 94 deletions(-) diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index aabb67dd4aa..7c9e3807260 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -834,6 +834,8 @@ export interface CodeActionProvider { displayName?: string; + extensionId?: string; + /** * Provide commands for the given document and range. */ diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 0253cba2d4e..3ac703098b1 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -367,9 +367,9 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread })); } - // --- quick fix + // --- code actions - $registerQuickFixSupport(handle: number, selector: IDocumentFilterDto[], metadata: ICodeActionProviderMetadataDto, displayName: string, supportsResolve: boolean): void { + $registerCodeActionSupport(handle: number, selector: IDocumentFilterDto[], metadata: ICodeActionProviderMetadataDto, displayName: string, extensionId: string, supportsResolve: boolean): void { const provider: languages.CodeActionProvider = { provideCodeActions: async (model: ITextModel, rangeOrSelection: EditorRange | Selection, context: languages.CodeActionContext, token: CancellationToken): Promise => { const listDto = await this._proxy.$provideCodeActions(handle, model.uri, rangeOrSelection, context, token); @@ -387,7 +387,8 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread }, providedCodeActionKinds: metadata.providedKinds, documentation: metadata.documentation, - displayName + displayName, + extensionId, }; if (supportsResolve) { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 9bf6786d9cf..4f6c27c82e3 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -414,7 +414,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $registerMultiDocumentHighlightProvider(handle: number, selector: IDocumentFilterDto[]): void; $registerLinkedEditingRangeProvider(handle: number, selector: IDocumentFilterDto[]): void; $registerReferenceSupport(handle: number, selector: IDocumentFilterDto[]): void; - $registerQuickFixSupport(handle: number, selector: IDocumentFilterDto[], metadata: ICodeActionProviderMetadataDto, displayName: string, supportsResolve: boolean): void; + $registerCodeActionSupport(handle: number, selector: IDocumentFilterDto[], metadata: ICodeActionProviderMetadataDto, displayName: string, extensionID: string, supportsResolve: boolean): void; $registerPasteEditProvider(handle: number, selector: IDocumentFilterDto[], metadata: IPasteEditProviderMetadataDto): void; $registerDocumentFormattingSupport(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier, displayName: string): void; $registerRangeFormattingSupport(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier, displayName: string, supportRanges: boolean): void; diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 2cb1dd5226a..3443d97a5dd 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -2196,6 +2196,10 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF return ext.displayName || ext.name; } + private static _extId(ext: IExtensionDescription): string { + return ext.identifier.value; + } + // --- outline registerDocumentSymbolProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.DocumentSymbolProvider, metadata?: vscode.DocumentSymbolProviderMetadata): vscode.Disposable { @@ -2385,18 +2389,18 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF return this._withAdapter(handle, ReferenceAdapter, adapter => adapter.provideReferences(URI.revive(resource), position, context, token), undefined, token); } - // --- quick fix + // --- code actions registerCodeActionProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.CodeActionProvider, metadata?: vscode.CodeActionProviderMetadata): vscode.Disposable { const store = new DisposableStore(); const handle = this._addNewAdapter(new CodeActionAdapter(this._documents, this._commands.converter, this._diagnostics, provider, this._logService, extension, this._apiDeprecation), extension); - this._proxy.$registerQuickFixSupport(handle, this._transformDocumentSelector(selector, extension), { + this._proxy.$registerCodeActionSupport(handle, this._transformDocumentSelector(selector, extension), { providedKinds: metadata?.providedCodeActionKinds?.map(kind => kind.value), documentation: metadata?.documentation?.map(x => ({ kind: x.kind.value, command: this._commands.converter.toInternal(x.command, store), })) - }, ExtHostLanguageFeatures._extLabel(extension), Boolean(provider.resolveCodeAction)); + }, ExtHostLanguageFeatures._extLabel(extension), ExtHostLanguageFeatures._extId(extension), Boolean(provider.resolveCodeAction)); store.add(this._createDisposable(handle)); return store; } diff --git a/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts b/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts index ca4254400e7..4339d4117f9 100644 --- a/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts +++ b/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts @@ -37,7 +37,7 @@ import { generateUuid } from 'vs/base/common/uuid'; type FormattingEditProvider = DocumentFormattingEditProvider | DocumentRangeFormattingEditProvider; -class DefaultFormatter extends Disposable implements IWorkbenchContribution { +export class DefaultFormatter extends Disposable implements IWorkbenchContribution { static readonly configName = 'editor.defaultFormatter'; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/saveParticipants/saveParticipants.ts b/src/vs/workbench/contrib/notebook/browser/contrib/saveParticipants/saveParticipants.ts index 2c4c71b98e5..89b90bbff4b 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/saveParticipants/saveParticipants.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/saveParticipants/saveParticipants.ts @@ -19,12 +19,12 @@ import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { ApplyCodeActionReason, applyCodeAction, getCodeActions } from 'vs/editor/contrib/codeAction/browser/codeAction'; -import { CodeActionKind, CodeActionTriggerSource } from 'vs/editor/contrib/codeAction/common/types'; +import { CodeActionItem, CodeActionKind, CodeActionTriggerSource } from 'vs/editor/contrib/codeAction/common/types'; import { FormattingMode, getDocumentFormattingEditsWithSelectedProvider } from 'vs/editor/contrib/format/browser/format'; import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { IProgress, IProgressStep } from 'vs/platform/progress/common/progress'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -32,6 +32,7 @@ import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/w import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchContributionsExtensions } from 'vs/workbench/common/contributions'; import { SaveReason } from 'vs/workbench/common/editor'; import { getNotebookEditorFromEditorPane } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { CellKind, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookFileWorkingCopyModel } from 'vs/workbench/contrib/notebook/common/notebookEditorModel'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -43,6 +44,7 @@ class FormatOnSaveParticipant implements IStoredFileWorkingCopySaveParticipant { constructor( @IEditorWorkerService private readonly editorWorkerService: IEditorWorkerService, @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, + @IInstantiationService private readonly instantiationService: IInstantiationService, @ITextModelService private readonly textModelService: ITextModelService, @IBulkEditService private readonly bulkEditService: IBulkEditService, @IConfigurationService private readonly configurationService: IConfigurationService, @@ -61,38 +63,40 @@ class FormatOnSaveParticipant implements IStoredFileWorkingCopySaveParticipant { if (!enabled) { return undefined; } + progress.report({ message: localize('notebookFormatSave.formatting', "Formatting") }); const notebook = workingCopy.model.notebookModel; + const formatApplied: boolean = await this.instantiationService.invokeFunction(CodeActionParticipantUtils.checkAndRunFormatCodeAction, notebook, progress, token); - progress.report({ message: localize('notebookFormatSave.formatting', "Formatting") }); const disposable = new DisposableStore(); try { - const allCellEdits = await Promise.all(notebook.cells.map(async cell => { - const ref = await this.textModelService.createModelReference(cell.uri); - disposable.add(ref); + if (!formatApplied) { + const allCellEdits = await Promise.all(notebook.cells.map(async cell => { + const ref = await this.textModelService.createModelReference(cell.uri); + disposable.add(ref); - const model = ref.object.textEditorModel; + const model = ref.object.textEditorModel; - const formatEdits = await getDocumentFormattingEditsWithSelectedProvider( - this.editorWorkerService, - this.languageFeaturesService, - model, - FormattingMode.Silent, - token - ); + const formatEdits = await getDocumentFormattingEditsWithSelectedProvider( + this.editorWorkerService, + this.languageFeaturesService, + model, + FormattingMode.Silent, + token + ); - const edits: ResourceTextEdit[] = []; + const edits: ResourceTextEdit[] = []; - if (formatEdits) { - edits.push(...formatEdits.map(edit => new ResourceTextEdit(model.uri, edit, model.getVersionId()))); - return edits; - } + if (formatEdits) { + edits.push(...formatEdits.map(edit => new ResourceTextEdit(model.uri, edit, model.getVersionId()))); + return edits; + } - return []; - })); - - await this.bulkEditService.apply(/* edit */allCellEdits.flat(), { label: localize('formatNotebook', "Format Notebook"), code: 'undoredo.formatNotebook', }); + return []; + })); + await this.bulkEditService.apply(/* edit */allCellEdits.flat(), { label: localize('formatNotebook', "Format Notebook"), code: 'undoredo.formatNotebook', }); + } } finally { progress.report({ increment: 100 }); disposable.dispose(); @@ -317,14 +321,12 @@ class CodeActionOnSaveParticipant implements IStoredFileWorkingCopySaveParticipa @IConfigurationService private readonly configurationService: IConfigurationService, @ILogService private readonly logService: ILogService, @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, - @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, @ITextModelService private readonly textModelService: ITextModelService, @IInstantiationService private readonly instantiationService: IInstantiationService, ) { } async participate(workingCopy: IStoredFileWorkingCopy, context: IStoredFileWorkingCopySaveParticipantContext, progress: IProgress, token: CancellationToken): Promise { - const nbDisposable = new DisposableStore(); const isTrusted = this.workspaceTrustManagementService.isWorkspaceTrusted(); if (!isTrusted) { return; @@ -350,15 +352,9 @@ class CodeActionOnSaveParticipant implements IStoredFileWorkingCopySaveParticipa const notebookModel = workingCopy.model.notebookModel; const setting = this.configurationService.getValue<{ [kind: string]: string | boolean }>(NotebookSetting.codeActionsOnSave); - if (!setting) { - return undefined; - } const settingItems: string[] = Array.isArray(setting) ? setting : Object.keys(setting).filter(x => setting[x]); - if (!settingItems.length) { - return undefined; - } const allCodeActions = this.createCodeActionsOnSave(settingItems); const excludedActions = allCodeActions @@ -368,60 +364,62 @@ class CodeActionOnSaveParticipant implements IStoredFileWorkingCopySaveParticipa const editorCodeActionsOnSave = includedActions.filter(x => !CodeActionKind.Notebook.contains(x)); const notebookCodeActionsOnSave = includedActions.filter(x => CodeActionKind.Notebook.contains(x)); - if (!editorCodeActionsOnSave.length && !notebookCodeActionsOnSave.length) { - return undefined; - } - - // prioritize `source.fixAll` code actions - if (!Array.isArray(setting)) { - editorCodeActionsOnSave.sort((a, b) => { - if (CodeActionKind.SourceFixAll.contains(a)) { - if (CodeActionKind.SourceFixAll.contains(b)) { - return 0; - } - return -1; - } - if (CodeActionKind.SourceFixAll.contains(b)) { - return 1; - } - return 0; - }); - } // run notebook code actions - progress.report({ message: localize('notebookSaveParticipants.notebookCodeActions', "Running 'Notebook' code actions") }); - try { - const cell = notebookModel.cells[0]; - const ref = await this.textModelService.createModelReference(cell.uri); - nbDisposable.add(ref); - - const textEditorModel = ref.object.textEditorModel; - - await this.applyOnSaveActions(textEditorModel, notebookCodeActionsOnSave, excludedActions, progress, token); - } catch { - this.logService.error('Failed to apply notebook code action on save'); - } finally { - progress.report({ increment: 100 }); - nbDisposable.dispose(); - } - - // run cell level code actions - const disposable = new DisposableStore(); - progress.report({ message: localize('notebookSaveParticipants.cellCodeActions', "Running 'Cell' code actions") }); - try { - await Promise.all(notebookModel.cells.map(async cell => { + if (notebookCodeActionsOnSave.length) { + const nbDisposable = new DisposableStore(); + progress.report({ message: localize('notebookSaveParticipants.notebookCodeActions', "Running 'Notebook' code actions") }); + try { + const cell = notebookModel.cells[0]; const ref = await this.textModelService.createModelReference(cell.uri); - disposable.add(ref); + nbDisposable.add(ref); const textEditorModel = ref.object.textEditorModel; - await this.applyOnSaveActions(textEditorModel, editorCodeActionsOnSave, excludedActions, progress, token); - })); - } catch { - this.logService.error('Failed to apply code action on save'); - } finally { - progress.report({ increment: 100 }); - disposable.dispose(); + await this.instantiationService.invokeFunction(CodeActionParticipantUtils.applyOnSaveGenericCodeActions, textEditorModel, notebookCodeActionsOnSave, excludedActions, progress, token); + } catch { + this.logService.error('Failed to apply notebook code action on save'); + } finally { + progress.report({ increment: 100 }); + nbDisposable.dispose(); + } + } + + // run cell level code actions + if (editorCodeActionsOnSave.length) { + // prioritize `source.fixAll` code actions + if (!Array.isArray(setting)) { + editorCodeActionsOnSave.sort((a, b) => { + if (CodeActionKind.SourceFixAll.contains(a)) { + if (CodeActionKind.SourceFixAll.contains(b)) { + return 0; + } + return -1; + } + if (CodeActionKind.SourceFixAll.contains(b)) { + return 1; + } + return 0; + }); + } + + const cellDisposable = new DisposableStore(); + progress.report({ message: localize('notebookSaveParticipants.cellCodeActions', "Running 'Cell' code actions") }); + try { + await Promise.all(notebookModel.cells.map(async cell => { + const ref = await this.textModelService.createModelReference(cell.uri); + cellDisposable.add(ref); + + const textEditorModel = ref.object.textEditorModel; + + await this.instantiationService.invokeFunction(CodeActionParticipantUtils.applyOnSaveGenericCodeActions, textEditorModel, editorCodeActionsOnSave, excludedActions, progress, token); + })); + } catch { + this.logService.error('Failed to apply code action on save'); + } finally { + progress.report({ increment: 100 }); + cellDisposable.dispose(); + } } } @@ -433,8 +431,52 @@ class CodeActionOnSaveParticipant implements IStoredFileWorkingCopySaveParticipa return kinds.every(otherKind => otherKind.equals(kind) || !otherKind.contains(kind)); }); } +} - private async applyOnSaveActions(model: ITextModel, codeActionsOnSave: readonly HierarchicalKind[], excludes: readonly HierarchicalKind[], progress: IProgress, token: CancellationToken): Promise { +class CodeActionParticipantUtils { + + static async checkAndRunFormatCodeAction( + accessor: ServicesAccessor, + notebookModel: NotebookTextModel, + progress: IProgress, + token: CancellationToken): Promise { + + const instantiationService: IInstantiationService = accessor.get(IInstantiationService); + const textModelService: ITextModelService = accessor.get(ITextModelService); + const logService: ILogService = accessor.get(ILogService); + const configurationService: IConfigurationService = accessor.get(IConfigurationService); + + const formatDisposable = new DisposableStore(); + let formatResult: boolean = false; + progress.report({ message: localize('notebookSaveParticipants.formatCodeActions', "Running 'Format' code actions") }); + try { + const cell = notebookModel.cells[0]; + const ref = await textModelService.createModelReference(cell.uri); + formatDisposable.add(ref); + const textEditorModel = ref.object.textEditorModel; + + const defaultFormatterExtId = configurationService.getValue(NotebookSetting.defaultFormatter); + formatResult = await instantiationService.invokeFunction(CodeActionParticipantUtils.applyOnSaveFormatCodeAction, textEditorModel, new HierarchicalKind('notebook.format'), [], defaultFormatterExtId, progress, token); + } catch { + logService.error('Failed to apply notebook format action on save'); + } finally { + progress.report({ increment: 100 }); + formatDisposable.dispose(); + } + return formatResult; + } + + static async applyOnSaveGenericCodeActions( + accessor: ServicesAccessor, + model: ITextModel, + codeActionsOnSave: readonly HierarchicalKind[], + excludes: readonly HierarchicalKind[], + progress: IProgress, + token: CancellationToken): Promise { + + const instantiationService: IInstantiationService = accessor.get(IInstantiationService); + const languageFeaturesService: ILanguageFeaturesService = accessor.get(ILanguageFeaturesService); + const logService: ILogService = accessor.get(ILogService); const getActionProgress = new class implements IProgress { private _names = new Set(); @@ -444,7 +486,7 @@ class CodeActionOnSaveParticipant implements IStoredFileWorkingCopySaveParticipa { key: 'codeaction.get2', comment: ['[configure]({1}) is a link. Only translate `configure`. Do not change brackets and parentheses or {1}'] }, "Getting code actions from '{0}' ([configure]({1})).", [...this._names].map(name => `'${name}'`).join(', '), - 'command:workbench.action.openSettings?%5B%22editor.codeActionsOnSave%22%5D' + 'command:workbench.action.openSettings?%5B%22notebook.codeActionsOnSave%22%5D' ) }); } @@ -457,7 +499,7 @@ class CodeActionOnSaveParticipant implements IStoredFileWorkingCopySaveParticipa }; for (const codeActionKind of codeActionsOnSave) { - const actionsToRun = await this.getActionsToRun(model, codeActionKind, excludes, getActionProgress, token); + const actionsToRun = await this.getActionsToRun(model, codeActionKind, excludes, languageFeaturesService, getActionProgress, token); if (token.isCancellationRequested) { actionsToRun.dispose(); return; @@ -480,11 +522,11 @@ class CodeActionOnSaveParticipant implements IStoredFileWorkingCopySaveParticipa } } if (breakFlag) { - this.logService.warn('Failed to apply code action on save, applied to multiple resources.'); + logService.warn('Failed to apply code action on save, applied to multiple resources.'); continue; } progress.report({ message: localize('codeAction.apply', "Applying code action '{0}'.", action.action.title) }); - await this.instantiationService.invokeFunction(applyCodeAction, action, ApplyCodeActionReason.OnSave, {}, token); + await instantiationService.invokeFunction(applyCodeAction, action, ApplyCodeActionReason.OnSave, {}, token); if (token.isCancellationRequested) { return; } @@ -497,13 +539,79 @@ class CodeActionOnSaveParticipant implements IStoredFileWorkingCopySaveParticipa } } - private getActionsToRun(model: ITextModel, codeActionKind: HierarchicalKind, excludes: readonly HierarchicalKind[], progress: IProgress, token: CancellationToken) { - return getCodeActions(this.languageFeaturesService.codeActionProvider, model, model.getFullModelRange(), { + static async applyOnSaveFormatCodeAction( + accessor: ServicesAccessor, + model: ITextModel, + formatCodeActionOnSave: HierarchicalKind, + excludes: readonly HierarchicalKind[], + extensionId: string | undefined, + progress: IProgress, + token: CancellationToken): Promise { + + const instantiationService: IInstantiationService = accessor.get(IInstantiationService); + const languageFeaturesService: ILanguageFeaturesService = accessor.get(ILanguageFeaturesService); + const logService: ILogService = accessor.get(ILogService); + + const getActionProgress = new class implements IProgress { + private _names = new Set(); + private _report(): void { + progress.report({ + message: localize( + { key: 'codeaction.get2', comment: ['[configure]({1}) is a link. Only translate `configure`. Do not change brackets and parentheses or {1}'] }, + "Getting code actions from '{0}' ([configure]({1})).", + [...this._names].map(name => `'${name}'`).join(', '), + 'command:workbench.action.openSettings?%5B%22notebook.defaultFormatter%22%5D' + ) + }); + } + report(provider: CodeActionProvider) { + if (provider.displayName && !this._names.has(provider.displayName)) { + this._names.add(provider.displayName); + this._report(); + } + } + }; + + const providedActions = await CodeActionParticipantUtils.getActionsToRun(model, formatCodeActionOnSave, excludes, languageFeaturesService, getActionProgress, token); + // warn the user if there are more than one provided format action, and there is no specified defaultFormatter + if (providedActions.validActions.length > 1 && !extensionId) { + logService.warn('More than one format code action is provided, the 0th one will be used. A default can be specified via `notebook.defaultFormatter` in your settings.'); + } + + if (token.isCancellationRequested) { + providedActions.dispose(); + return false; + } + + try { + const action: CodeActionItem | undefined = extensionId ? providedActions.validActions.find(action => action.provider?.extensionId === extensionId) : providedActions.validActions[0]; + if (!action) { + return false; + } + + progress.report({ message: localize('codeAction.apply', "Applying code action '{0}'.", action.action.title) }); + await instantiationService.invokeFunction(applyCodeAction, action, ApplyCodeActionReason.OnSave, {}, token); + if (token.isCancellationRequested) { + return false; + } + } catch { + logService.error('Failed to apply notebook format code action on save'); + return false; + } finally { + providedActions.dispose(); + } + return true; + } + + // @Yoyokrazy this could likely be modified to leverage the extensionID, therefore not getting actions from providers unnecessarily -- future work + static getActionsToRun(model: ITextModel, codeActionKind: HierarchicalKind, excludes: readonly HierarchicalKind[], languageFeaturesService: ILanguageFeaturesService, progress: IProgress, token: CancellationToken) { + return getCodeActions(languageFeaturesService.codeActionProvider, model, model.getFullModelRange(), { type: CodeActionTriggerType.Invoke, triggerAction: CodeActionTriggerSource.OnSave, filter: { include: codeActionKind, excludes: excludes, includeSourceActions: true }, }, progress, token); } + } function getActiveCellCodeEditor(editorService: IEditorService): ICodeEditor | undefined { diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 72eb95a92cf..67c09c27463 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -122,6 +122,7 @@ import { NotebookVariables } from 'vs/workbench/contrib/notebook/browser/contrib import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; import { NotebookAccessibilityHelp } from 'vs/workbench/contrib/notebook/browser/notebookAccessibilityHelp'; import { NotebookAccessibleView } from 'vs/workbench/contrib/notebook/browser/notebookAccessibleView'; +import { DefaultFormatter } from 'vs/workbench/contrib/format/browser/formatActionsMultiple'; /*--------------------------------------------------------------------------------------------- */ @@ -983,6 +984,14 @@ configurationRegistry.registerConfiguration({ tags: ['notebookLayout', 'notebookOutputLayout'], default: false }, + [NotebookSetting.defaultFormatter]: { + description: nls.localize('notebookFormatter.default', "Defines a default notebook formatter which takes precedence over all other formatter settings. Must be the identifier of an extension contributing a formatter."), + type: ['string', 'null'], + default: null, + enum: DefaultFormatter.extensionIds, + enumItemLabels: DefaultFormatter.extensionItemLabels, + markdownEnumDescriptions: DefaultFormatter.extensionDescriptions + }, [NotebookSetting.formatOnSave]: { markdownDescription: nls.localize('notebook.formatOnSave', "Format a notebook on save. A formatter must be available, the file must not be saved after delay, and the editor must not be shutting down."), type: 'boolean', diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index a9c0398c1a7..7828091e598 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -941,6 +941,7 @@ export const NotebookSetting = { minimalErrorRendering: 'notebook.output.minimalErrorRendering', formatOnSave: 'notebook.formatOnSave.enabled', insertFinalNewline: 'notebook.insertFinalNewline', + defaultFormatter: 'notebook.defaultFormatter', formatOnCellExecution: 'notebook.formatOnCellExecution', codeActionsOnSave: 'notebook.codeActionsOnSave', outputWordWrap: 'notebook.output.wordWrap', From a48f464a3e01aad384703ec964018299b14bb7cf Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Thu, 16 May 2024 16:03:53 -0700 Subject: [PATCH 250/357] Drop usage of inline chat services in notebook chat. (#212935) --- .../controller/chat/cellChatActions.ts | 67 +-- .../controller/chat/notebookChatController.ts | 445 +++++++----------- 2 files changed, 163 insertions(+), 349 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts index f640be27a6f..7289f19d20b 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts @@ -15,8 +15,8 @@ import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { InputFocusedContextKey } from 'vs/platform/contextkey/common/contextkeys'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_PROVIDER, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_LAST_RESPONSE_TYPE, CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChatResponseFeedbackKind, InlineChatResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; -import { CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST, CTX_NOTEBOOK_CHAT_OUTER_FOCUS_POSITION, CTX_NOTEBOOK_CHAT_USER_DID_EDIT, MENU_CELL_CHAT_INPUT, MENU_CELL_CHAT_WIDGET, MENU_CELL_CHAT_WIDGET_FEEDBACK, MENU_CELL_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/notebook/browser/controller/chat/notebookChatContext'; +import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_PROVIDER, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChatResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST, CTX_NOTEBOOK_CHAT_OUTER_FOCUS_POSITION, CTX_NOTEBOOK_CHAT_USER_DID_EDIT, MENU_CELL_CHAT_INPUT, MENU_CELL_CHAT_WIDGET, MENU_CELL_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/notebook/browser/controller/chat/notebookChatContext'; import { NotebookChatController } from 'vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController'; import { CELL_TITLE_CELL_GROUP_ID, INotebookActionContext, INotebookCellActionContext, NotebookAction, NotebookCellAction, getEditorFromArgsOrActivePane } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; import { insertNewCell } from 'vs/workbench/contrib/notebook/browser/controller/insertCellActions'; @@ -299,69 +299,6 @@ registerAction2(class extends NotebookAction { } }); -registerAction2(class extends NotebookAction { - constructor() { - super({ - id: 'notebook.cell.feedbackHelpful', - title: localize('feedback.helpful', 'Helpful'), - icon: Codicon.thumbsup, - menu: { - id: MENU_CELL_CHAT_WIDGET_FEEDBACK, - group: 'inline', - order: 1, - when: CTX_INLINE_CHAT_LAST_RESPONSE_TYPE.notEqualsTo(undefined), - }, - f1: false - }); - } - - async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext) { - NotebookChatController.get(context.notebookEditor)?.feedbackLast(InlineChatResponseFeedbackKind.Helpful); - } -}); - -registerAction2(class extends NotebookAction { - constructor() { - super({ - id: 'notebook.cell.feedbackUnhelpful', - title: localize('feedback.unhelpful', 'Unhelpful'), - icon: Codicon.thumbsdown, - menu: { - id: MENU_CELL_CHAT_WIDGET_FEEDBACK, - group: 'inline', - order: 2, - when: CTX_INLINE_CHAT_LAST_RESPONSE_TYPE.notEqualsTo(undefined), - }, - f1: false - }); - } - - async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext) { - NotebookChatController.get(context.notebookEditor)?.feedbackLast(InlineChatResponseFeedbackKind.Unhelpful); - } -}); - -registerAction2(class extends NotebookAction { - constructor() { - super({ - id: 'notebook.cell.reportIssueForBug', - title: localize('feedback.reportIssueForBug', 'Report Issue'), - icon: Codicon.report, - menu: { - id: MENU_CELL_CHAT_WIDGET_FEEDBACK, - group: 'inline', - order: 3, - when: CTX_INLINE_CHAT_LAST_RESPONSE_TYPE.notEqualsTo(undefined), - }, - f1: false - }); - } - - async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext) { - NotebookChatController.get(context.notebookEditor)?.feedbackLast(InlineChatResponseFeedbackKind.Bug); - } -}); - interface IInsertCellWithChatArgs extends INotebookActionContext { input?: string; autoSend?: boolean; diff --git a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts index 1df296db174..a2ac8fcf663 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts @@ -4,18 +4,16 @@ *--------------------------------------------------------------------------------------------*/ import { Dimension, IFocusTracker, WindowIntervalTimer, getWindow, scheduleAtNextAnimationFrame, trackFocus } from 'vs/base/browser/dom'; -import { CancelablePromise, Queue, createCancelablePromise, disposableTimeout, raceCancellationError } from 'vs/base/common/async'; +import { CancelablePromise, DeferredPromise, Queue, createCancelablePromise, disposableTimeout } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { Emitter, Event } from 'vs/base/common/event'; -import { MarkdownString } from 'vs/base/common/htmlContent'; -import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { Emitter } from 'vs/base/common/event'; +import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { LRUCache } from 'vs/base/common/map'; import { Schemas } from 'vs/base/common/network'; import { MovingAverage } from 'vs/base/common/numbers'; import { StopWatch } from 'vs/base/common/stopwatch'; import { assertType } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; -import { generateUuid } from 'vs/base/common/uuid'; import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; @@ -27,22 +25,18 @@ import { ICursorStateComputer, ITextModel } from 'vs/editor/common/model'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { IModelService } from 'vs/editor/common/services/model'; import { localize } from 'vs/nls'; -import { ICommandService } from 'vs/platform/commands/common/commands'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { AsyncProgress } from 'vs/platform/progress/common/progress'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { SaveReason } from 'vs/workbench/common/editor'; import { GeneratingPhrase } from 'vs/workbench/contrib/chat/browser/chat'; -import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatAgentLocation, IChatAgentRequest, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatModel, ChatRequestModel, getHistoryEntriesFromModel, IChatRequestVariableData, IChatResponseModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import { IChatProgress, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; -import { IInlineChatSavingService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSavingService'; -import { EmptyResponse, ErrorResponse, ReplyResponse, Session, SessionExchange, SessionPrompt } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; -import { IInlineChatSessionService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionService'; import { ProgressingEditsOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatStrategies'; -import { IInlineChatMessageAppender, InlineChatWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; +import { InlineChatWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; import { asProgressiveEdit, performAsyncTextEdit } from 'vs/workbench/contrib/inlineChat/browser/utils'; -import { CTX_INLINE_CHAT_LAST_RESPONSE_TYPE, EditMode, IInlineChatProgressItem, IInlineChatRequest, InlineChatResponseFeedbackKind, InlineChatResponseType } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { insertCell, runDeleteAction } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations'; import { CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST, CTX_NOTEBOOK_CHAT_OUTER_FOCUS_POSITION, CTX_NOTEBOOK_CHAT_USER_DID_EDIT, MENU_CELL_CHAT_INPUT, MENU_CELL_CHAT_WIDGET, MENU_CELL_CHAT_WIDGET_FEEDBACK, MENU_CELL_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/notebook/browser/controller/chat/notebookChatContext'; import { ICellViewModel, INotebookEditor, INotebookEditorContribution, INotebookViewZone } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; @@ -85,17 +79,35 @@ class NotebookChatWidget extends Disposable implements INotebookViewZone { ) { super(); - this._register(inlineChatWidget.onDidChangeHeight(() => { + const updateHeight = () => { + if (this.heightInPx === inlineChatWidget.contentHeight) { + return; + } + this.heightInPx = inlineChatWidget.contentHeight; this._notebookEditor.changeViewZones(accessor => { accessor.layoutZone(id); }); this._layoutWidget(inlineChatWidget, widgetContainer); + }; + + this._register(inlineChatWidget.onDidChangeHeight(() => { + updateHeight(); })); + this._register(inlineChatWidget.chatWidget.onDidChangeHeight(() => { + console.log('chat widget height changed', inlineChatWidget.chatWidget.contentHeight); + updateHeight(); + })); + + this.heightInPx = inlineChatWidget.contentHeight; this._layoutWidget(inlineChatWidget, widgetContainer); } + layout() { + this._layoutWidget(this.inlineChatWidget, this.widgetContainer); + } + restoreEditingCell(initEditingCell: ICellViewModel) { this._editingCell = initEditingCell; @@ -245,7 +257,6 @@ export class NotebookChatController extends Disposable implements INotebookEdito private _strategy: EditStrategy | undefined; private _sessionCtor: CancelablePromise | undefined; - private _activeSession?: Session; private _warmupRequestCts?: CancellationTokenSource; private _activeRequestCts?: CancellationTokenSource; private readonly _ctxHasActiveRequest: IContextKey; @@ -253,28 +264,29 @@ export class NotebookChatController extends Disposable implements INotebookEdito private readonly _ctxUserDidEdit: IContextKey; private readonly _ctxOuterFocusPosition: IContextKey<'above' | 'below' | ''>; private readonly _userEditingDisposables = this._register(new DisposableStore()); - private readonly _ctxLastResponseType: IContextKey; - private _widget: NotebookChatWidget | undefined; private readonly _widgetDisposableStore = this._register(new DisposableStore()); private _focusTracker: IFocusTracker | undefined; + private _widget: NotebookChatWidget | undefined; + + private _notebookDefaultAgentId: string | undefined; + private readonly _model: MutableDisposable = this._register(new MutableDisposable()); + private _currentRequest: ChatRequestModel | undefined; constructor( private readonly _notebookEditor: INotebookEditor, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IInlineChatSessionService private readonly _inlineChatSessionService: IInlineChatSessionService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, - @ICommandService private readonly _commandService: ICommandService, @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, - @IInlineChatSavingService private readonly _inlineChatSavingService: IInlineChatSavingService, @IModelService private readonly _modelService: IModelService, @ILanguageService private readonly _languageService: ILanguageService, @INotebookExecutionStateService private _executionStateService: INotebookExecutionStateService, @IStorageService private readonly _storageService: IStorageService, + @IChatAgentService private readonly _chatAgentService: IChatAgentService, + @IChatService private readonly _chatService: IChatService, ) { super(); this._ctxHasActiveRequest = CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST.bindTo(this._contextKeyService); this._ctxCellWidgetFocused = CTX_NOTEBOOK_CELL_CHAT_FOCUSED.bindTo(this._contextKeyService); - this._ctxLastResponseType = CTX_INLINE_CHAT_LAST_RESPONSE_TYPE.bindTo(this._contextKeyService); this._ctxUserDidEdit = CTX_NOTEBOOK_CHAT_USER_DID_EDIT.bindTo(this._contextKeyService); this._ctxOuterFocusPosition = CTX_NOTEBOOK_CHAT_OUTER_FOCUS_POSITION.bindTo(this._contextKeyService); @@ -291,6 +303,20 @@ export class NotebookChatController extends Disposable implements INotebookEdito this._historyCandidate = ''; this._storageService.store(NotebookChatController._storageKey, JSON.stringify(NotebookChatController._promptHistory), StorageScope.PROFILE, StorageTarget.USER); }; + + if (!this._initNotebookAgent()) { + this._register(this._chatAgentService.onDidChangeAgents(() => this._initNotebookAgent())); + } + } + + private _initNotebookAgent(): boolean { + const notebookAgent = this._chatAgentService.getContributedDefaultAgent(ChatAgentLocation.Notebook); + if (notebookAgent) { + this._notebookDefaultAgentId = notebookAgent.id; + return true; + } + + return false; } private _registerFocusTracker() { @@ -457,14 +483,7 @@ export class NotebookChatController extends Disposable implements INotebookEdito this._sessionCtor = createCancelablePromise(async token => { if (fakeParentEditor.hasModel()) { - await this._startSession(fakeParentEditor, token); - this._warmupRequestCts = new CancellationTokenSource(); - this._startInitialFolowups(fakeParentEditor, this._warmupRequestCts.token); - if (this._widget) { - this._widget.inlineChatWidget.placeholder = this._activeSession?.session.placeholder ?? localize('default.placeholder', "Ask a question"); - this._widget.inlineChatWidget.updateInfo(this._activeSession?.session.message ?? localize('welcome.1', "AI-generated code may be incorrect")); - this._widget.inlineChatWidget.updateSlashCommands(this._activeSession?.session.slashCommands ?? []); this._focusWidget(); } @@ -515,21 +534,14 @@ export class NotebookChatController extends Disposable implements INotebookEdito async acceptInput() { assertType(this._widget); - await this._sessionCtor; - assertType(this._activeSession); - this._warmupRequestCts?.dispose(true); - this._warmupRequestCts = undefined; - this._activeSession.addInput(new SessionPrompt(this._widget.inlineChatWidget.value)); - assertType(this._activeSession.lastInput); - const value = this._activeSession.lastInput.value; - - this._historyUpdate(value); + const lastInput = this._widget.inlineChatWidget.value; + this._historyUpdate(lastInput); const editor = this._widget.parentEditor; - const model = editor.getModel(); + const textModel = editor.getModel(); - if (!editor.hasModel() || !model) { + if (!editor.hasModel() || !textModel) { return; } @@ -554,217 +566,134 @@ export class NotebookChatController extends Disposable implements INotebookEdito } this._ctxHasActiveRequest.set(true); - this._widget.inlineChatWidget.updateSlashCommands(this._activeSession.session.slashCommands ?? []); - this._widget?.inlineChatWidget.updateProgress(true); - const request: IInlineChatRequest = { - requestId: generateUuid(), - prompt: value, - attempt: 0, - selection: { selectionStartLineNumber: 1, selectionStartColumn: 1, positionLineNumber: 1, positionColumn: 1 }, - wholeRange: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }, - live: true, - previewDocument: model.uri, - withIntentDetection: true, // TODO: don't hard code but allow in corresponding UI to run without intent detection? + // Start a new session + + if (!this._model.value) { + this._model.value = this._chatService.startSession(ChatAgentLocation.Editor, CancellationToken.None); + if (!this._model.value) { + throw new Error('Failed to start chat session'); + } + } + + this._strategy = new EditStrategy(); + + const model = this._model.value; + this._widget.inlineChatWidget.setChatModel(model); + + const request: IParsedChatRequest = { + text: lastInput, + parts: [] }; - //TODO: update progress in a newly inserted cell below the widget instead of the fake editor + const requestVarData: IChatRequestVariableData = { + variables: [] + }; + + this._currentRequest = model.addRequest(request, requestVarData, 0); + const responseCreated = new DeferredPromise(); + let responseCreatedComplete = false; + const completeResponseCreated = () => { + if (!responseCreatedComplete && this._currentRequest?.response) { + responseCreated.complete(this._currentRequest.response); + responseCreatedComplete = true; + } + }; this._activeRequestCts?.cancel(); this._activeRequestCts = new CancellationTokenSource(); - const progressEdits: TextEdit[][] = []; + const cancellationToken = new CancellationTokenSource().token; const progressiveEditsQueue = new Queue(); const progressiveEditsClock = StopWatch.create(); const progressiveEditsAvgDuration = new MovingAverage(); const progressiveEditsCts = new CancellationTokenSource(this._activeRequestCts.token); - let progressiveChatResponse: IInlineChatMessageAppender | undefined; - const progress = new AsyncProgress(async data => { - // console.log('received chunk', data, request); - - if (this._activeRequestCts?.token.isCancellationRequested) { + const progressCallback = (progress: IChatProgress) => { + if (cancellationToken.isCancellationRequested) { return; } - if (data.message) { - this._widget?.inlineChatWidget.updateToolbar(false); - this._widget?.inlineChatWidget.updateInfo(data.message); - } + if (this._currentRequest) { + if (progress.kind === 'textEdit') { + progressiveEditsAvgDuration.update(progressiveEditsClock.elapsed()); + progressiveEditsClock.reset(); - if (data.edits?.length) { - if (!request.live) { - throw new Error('Progress in NOT supported in non-live mode'); - } - progressEdits.push(data.edits); - progressiveEditsAvgDuration.update(progressiveEditsClock.elapsed()); - progressiveEditsClock.reset(); + progressiveEditsQueue.queue(async () => { + // making changes goes into a queue because otherwise the async-progress time will + // influence the time it takes to receive the changes and progressive typing will + // become infinitely fast - progressiveEditsQueue.queue(async () => { - // making changes goes into a queue because otherwise the async-progress time will - // influence the time it takes to receive the changes and progressive typing will - // become infinitely fast - await this._makeChanges(data.edits!, data.editsShouldBeInstant - ? undefined - : { duration: progressiveEditsAvgDuration.value, token: progressiveEditsCts.token } - ); - }); - } - - if (data.markdownFragment) { - if (!progressiveChatResponse) { - const message = { - message: new MarkdownString(data.markdownFragment, { supportThemeIcons: true, supportHtml: true, isTrusted: false }), - requestId: request.requestId, - }; - progressiveChatResponse = this._widget?.inlineChatWidget.updateChatMessage(message, true); + await this._makeChanges(progress.edits!, false + ? undefined + : { duration: progressiveEditsAvgDuration.value, token: progressiveEditsCts.token } + ); + }); } else { - progressiveChatResponse.appendContent(data.markdownFragment); + model.acceptResponseProgress(this._currentRequest, progress); } + completeResponseCreated(); } - }); - - const task = this._activeSession.provider.provideResponse(this._activeSession.session, request, progress, this._activeRequestCts.token); - let response: ReplyResponse | ErrorResponse | EmptyResponse; - - try { - this._widget?.inlineChatWidget.updateChatMessage(undefined); - this._widget?.inlineChatWidget.updateFollowUps(undefined); - this._widget?.inlineChatWidget.updateProgress(true); - this._widget?.inlineChatWidget.updateInfo(!this._activeSession.lastExchange ? GeneratingPhrase + '\u2026' : ''); - this._ctxHasActiveRequest.set(true); - - const reply = await raceCancellationError(Promise.resolve(task), this._activeRequestCts.token); - if (progressiveEditsQueue.size > 0) { - // we must wait for all edits that came in via progress to complete - await Event.toPromise(progressiveEditsQueue.onDrained); - } - await progress.drain(); - - if (!reply) { - response = new EmptyResponse(); - } else { - const markdownContents = new MarkdownString('', { supportThemeIcons: true, supportHtml: true, isTrusted: false }); - const replyResponse = response = this._instantiationService.createInstance(ReplyResponse, reply, markdownContents, this._activeSession.textModelN.uri, this._activeSession.textModelN.getAlternativeVersionId(), progressEdits, request.requestId, undefined); - for (let i = progressEdits.length; i < replyResponse.allLocalEdits.length; i++) { - await this._makeChanges(replyResponse.allLocalEdits[i], undefined); - } - - if (this._activeSession?.provider.provideFollowups) { - const followupCts = new CancellationTokenSource(); - const followups = await this._activeSession.provider.provideFollowups(this._activeSession.session, replyResponse.raw, followupCts.token); - if (followups && this._widget) { - const widget = this._widget; - widget.inlineChatWidget.updateFollowUps(followups, async followup => { - if (followup.kind === 'reply') { - widget.inlineChatWidget.value = followup.message; - this.acceptInput(); - } else { - await this.acceptSession(); - this._commandService.executeCommand(followup.commandId, ...(followup.args ?? [])); - } - }); - } - } - - this._userEditingDisposables.clear(); - // monitor user edits - const editingCell = this._widget.getEditingCell(); - if (editingCell) { - this._userEditingDisposables.add(editingCell.model.onDidChangeContent(() => this._updateUserEditingState())); - this._userEditingDisposables.add(editingCell.model.onDidChangeLanguage(() => this._updateUserEditingState())); - this._userEditingDisposables.add(editingCell.model.onDidChangeMetadata(() => this._updateUserEditingState())); - this._userEditingDisposables.add(editingCell.model.onDidChangeInternalMetadata(() => this._updateUserEditingState())); - this._userEditingDisposables.add(editingCell.model.onDidChangeOutputs(() => this._updateUserEditingState())); - this._userEditingDisposables.add(this._executionStateService.onDidChangeExecution(e => { - if (e.type === NotebookExecutionType.cell && e.affectsCell(editingCell.uri)) { - this._updateUserEditingState(); - } - })); - } - } - } catch (e) { - response = new ErrorResponse(e); - } finally { - this._ctxHasActiveRequest.set(false); - this._widget?.inlineChatWidget.updateProgress(false); - this._widget?.inlineChatWidget.updateInfo(''); - this._widget?.inlineChatWidget.updateToolbar(true); - } - - this._ctxHasActiveRequest.set(false); - this._widget?.inlineChatWidget.updateProgress(false); - this._widget?.inlineChatWidget.updateInfo(''); - this._widget?.inlineChatWidget.updateToolbar(true); - - this._activeSession?.addExchange(new SessionExchange(this._activeSession.lastInput, response)); - this._ctxLastResponseType.set(response instanceof ReplyResponse ? response.raw.type : undefined); - } - - private async _startSession(editor: IActiveCodeEditor, token: CancellationToken) { - if (this._activeSession) { - this._inlineChatSessionService.releaseSession(this._activeSession); - } - - const session = await this._inlineChatSessionService.createSession( - editor, - { editMode: EditMode.Live }, - token - ); - - if (!session) { - return; - } - - this._activeSession = session; - this._strategy = new EditStrategy(session); - } - - private async _startInitialFolowups(editor: IActiveCodeEditor, token: CancellationToken) { - if (!this._activeSession || !this._activeSession.provider.provideFollowups) { - return; - } - - const request: IInlineChatRequest = { - requestId: generateUuid(), - prompt: '', - attempt: 0, - selection: { selectionStartLineNumber: 1, selectionStartColumn: 1, positionLineNumber: 1, positionColumn: 1 }, - wholeRange: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }, - live: true, - previewDocument: editor.getModel().uri, - withIntentDetection: true }; - const progress = new AsyncProgress(async data => { }); - const task = this._activeSession.provider.provideResponse(this._activeSession.session, request, progress, token); - const reply = await raceCancellationError(Promise.resolve(task), token); - if (token.isCancellationRequested) { - return; + await model.waitForInitialization(); + this._widget.inlineChatWidget.addToHistory(lastInput); + + completeResponseCreated(); + const requestProps: IChatAgentRequest = { + sessionId: model.sessionId, + requestId: this._currentRequest!.id, + agentId: this._notebookDefaultAgentId!, + message: lastInput, + variables: { + variables: [{ + id: '_notebookChatInput', + name: '_notebookChatInput', + value: this._widget.parentEditor.getModel()!.uri, + }] + }, + location: ChatAgentLocation.Notebook + }; + try { + this._ctxHasActiveRequest.set(true); + + const task = this._chatAgentService.invokeAgent(this._notebookDefaultAgentId!, requestProps, progressCallback, getHistoryEntriesFromModel(model, this._notebookDefaultAgentId!), cancellationToken); + this._widget.inlineChatWidget.updateChatMessage(undefined); + this._widget.inlineChatWidget.updateFollowUps(undefined); + this._widget.inlineChatWidget.updateProgress(true); + this._widget.inlineChatWidget.updateInfo(GeneratingPhrase + '\u2026'); + await task; + + this._userEditingDisposables.clear(); + // monitor user edits + const editingCell = this._widget.getEditingCell(); + if (editingCell) { + this._userEditingDisposables.add(editingCell.model.onDidChangeContent(() => this._updateUserEditingState())); + this._userEditingDisposables.add(editingCell.model.onDidChangeLanguage(() => this._updateUserEditingState())); + this._userEditingDisposables.add(editingCell.model.onDidChangeMetadata(() => this._updateUserEditingState())); + this._userEditingDisposables.add(editingCell.model.onDidChangeInternalMetadata(() => this._updateUserEditingState())); + this._userEditingDisposables.add(editingCell.model.onDidChangeOutputs(() => this._updateUserEditingState())); + this._userEditingDisposables.add(this._executionStateService.onDidChangeExecution(e => { + if (e.type === NotebookExecutionType.cell && e.affectsCell(editingCell.uri)) { + this._updateUserEditingState(); + } + })); + } + } catch (e) { + } finally { + this._ctxHasActiveRequest.set(false); + this._widget.inlineChatWidget.updateProgress(false); + this._widget.inlineChatWidget.updateInfo(''); + this._widget.inlineChatWidget.updateToolbar(true); + if (this._currentRequest) { + model.completeResponse(this._currentRequest); + completeResponseCreated(); + } } - if (!reply) { - return; - } - - const markdownContents = new MarkdownString('', { supportThemeIcons: true, supportHtml: true, isTrusted: false }); - const response = this._instantiationService.createInstance(ReplyResponse, reply, markdownContents, this._activeSession.textModelN.uri, this._activeSession.textModelN.getAlternativeVersionId(), [], request.requestId, undefined); - const followups = await this._activeSession.provider.provideFollowups(this._activeSession.session, response.raw, token); - if (followups && this._widget) { - const widget = this._widget; - widget.inlineChatWidget.updateFollowUps(followups, async followup => { - if (followup.kind === 'reply') { - widget.inlineChatWidget.value = followup.message; - this.acceptInput(); - } else { - await this.acceptSession(); - this._commandService.executeCommand(followup.commandId, ...(followup.args ?? [])); - } - }); - } + return responseCreated.p; } private async _makeChanges(edits: TextEdit[], opts: ProgressingEditsOptions | undefined) { - assertType(this._activeSession); assertType(this._strategy); assertType(this._widget); @@ -787,18 +716,13 @@ export class NotebookChatController extends Disposable implements INotebookEdito const actualEdits = !opts && moreMinimalEdits ? moreMinimalEdits : edits; const editOperations = actualEdits.map(TextEdit.asEditOperation); - this._inlineChatSavingService.markChanged(this._activeSession); try { - // this._ignoreModelContentChanged = true; - this._activeSession.wholeRange.trackEdits(editOperations); if (opts) { await this._strategy.makeProgressiveChanges(editor, editOperations, opts); } else { await this._strategy.makeChanges(editor, editOperations); } - // this._ctxDidEdit.set(this._activeSession.hasChangedText); } finally { - // this._ignoreModelContentChanged = false; } } @@ -807,7 +731,7 @@ export class NotebookChatController extends Disposable implements INotebookEdito } async acceptSession() { - assertType(this._activeSession); + assertType(this._model); assertType(this._strategy); const editor = this._widget?.parentEditor; @@ -817,16 +741,16 @@ export class NotebookChatController extends Disposable implements INotebookEdito const editingCell = this._widget?.getEditingCell(); - if (editingCell && this._notebookEditor.hasModel() && this._activeSession.lastInput) { + if (editingCell && this._notebookEditor.hasModel()) { const cellId = NotebookCellTextModelLikeId.str({ uri: editingCell.uri, viewType: this._notebookEditor.textModel.viewType }); - const prompt = this._activeSession.lastInput.value; - this._promptCache.set(cellId, prompt); + if (this._widget?.inlineChatWidget.value) { + this._promptCache.set(cellId, this._widget.inlineChatWidget.value); + } this._onDidChangePromptCache.fire({ cell: editingCell.uri }); } try { - await this._strategy.apply(editor); - this._inlineChatSessionService.releaseSession(this._activeSession); + this._model.clear(); } catch (_err) { } this.dismiss(false); @@ -925,10 +849,6 @@ export class NotebookChatController extends Disposable implements INotebookEdito } async cancelCurrentRequest(discard: boolean) { - if (discard) { - this._strategy?.cancel(); - } - this._activeRequestCts?.cancel(); } @@ -937,20 +857,11 @@ export class NotebookChatController extends Disposable implements INotebookEdito } discard() { - this._strategy?.cancel(); this._activeRequestCts?.cancel(); this._widget?.discardChange(); this.dismiss(true); } - async feedbackLast(kind: InlineChatResponseFeedbackKind) { - if (this._activeSession?.lastExchange && this._activeSession.lastExchange.response instanceof ReplyResponse) { - this._activeSession.provider.handleInlineChatResponseFeedback?.(this._activeSession.session, this._activeSession.lastExchange.response.raw, kind); - this._widget?.inlineChatWidget.updateStatus('Thank you for your feedback!', { resetAfter: 1250 }); - } - } - - dismiss(discard: boolean) { const widget = this._widget; const widgetIndex = widget?.afterModelPosition; @@ -1012,10 +923,7 @@ export class NotebookChatController extends Disposable implements INotebookEdito export class EditStrategy { private _editCount: number = 0; - constructor( - protected readonly _session: Session, - ) { - + constructor() { } async makeProgressiveChanges(editor: IActiveCodeEditor, edits: ISingleEditOperation[], opts: ProgressingEditsOptions): Promise { @@ -1049,37 +957,6 @@ export class EditStrategy { } editor.executeEdits('inline-chat-live', edits, cursorStateComputerAndInlineDiffCollection); } - - async apply(editor: IActiveCodeEditor) { - if (this._editCount > 0) { - editor.pushUndoStop(); - } - if (!(this._session.lastExchange?.response instanceof ReplyResponse)) { - return; - } - const { untitledTextModel } = this._session.lastExchange.response; - if (untitledTextModel && !untitledTextModel.isDisposed() && untitledTextModel.isDirty()) { - await untitledTextModel.save({ reason: SaveReason.EXPLICIT }); - } - } - - async cancel() { - const { textModelN: modelN, textModelNAltVersion, textModelNSnapshotAltVersion } = this._session; - if (modelN.isDisposed()) { - return; - } - - const targetAltVersion = textModelNSnapshotAltVersion ?? textModelNAltVersion; - while (targetAltVersion < modelN.getAlternativeVersionId() && modelN.canUndo()) { - modelN.undo(); - } - } - - createSnapshot(): void { - if (this._session && !this._session.textModel0.equalsTextBuffer(this._session.textModelN.getTextBuffer())) { - this._session.createSnapshot(); - } - } } From 8a7971c6070022bd2a95a7807abf6b3c2ecf06f9 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Thu, 16 May 2024 23:00:13 -0700 Subject: [PATCH 251/357] fix: only include files that exist on disk in Attach Context picker (#212942) --- src/vs/platform/quickinput/common/quickAccess.ts | 1 + .../contrib/chat/browser/actions/chatContextActions.ts | 9 ++++++++- .../contrib/search/browser/anythingQuickAccess.ts | 7 +++++-- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/vs/platform/quickinput/common/quickAccess.ts b/src/vs/platform/quickinput/common/quickAccess.ts index c160bb1fb93..a8256454a91 100644 --- a/src/vs/platform/quickinput/common/quickAccess.ts +++ b/src/vs/platform/quickinput/common/quickAccess.ts @@ -22,6 +22,7 @@ export interface IQuickAccessProviderRunOptions { */ export interface AnythingQuickAccessProviderRunOptions extends IQuickAccessProviderRunOptions { readonly includeHelp?: boolean; + readonly filter?: (item: unknown) => boolean; /** * @deprecated - temporary for Dynamic Chat Variables (see usage) until it has built-in UX for file picking * Useful for adding items to the top of the list that might contain actions. diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index 4827b26a7b9..4b773fd2c57 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -5,6 +5,7 @@ import { Codicon } from 'vs/base/common/codicons'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { localize2 } from 'vs/nls'; @@ -65,7 +66,13 @@ class AttachContextAction extends Action2 { const picks = await quickInputService.quickAccess.pick('', { providerOptions: { - additionPicks: quickPickItems + additionPicks: quickPickItems, + filter: (item) => { + if (item && typeof item === 'object' && 'resource' in item && URI.isUri(item.resource)) { + return [Schemas.file, Schemas.vscodeRemote].includes(item.resource.scheme); + } + return true; + } } }); diff --git a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts index 1dad72b865b..66badbec7ae 100644 --- a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts @@ -364,7 +364,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider options.filter?.(p)) : picks, // Slow picks: files and symbols additionalPicks: (async (): Promise> => { @@ -377,7 +377,10 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider options.filter?.(p)); + } if (token.isCancellationRequested) { return []; } From a47a7231ab797f0e9aaf483ae20cbcfdb86f9226 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 17 May 2024 08:39:49 +0200 Subject: [PATCH 252/357] watcher - per-request restart support (#212946) --- src/vs/platform/files/common/watcher.ts | 66 ++++++++++++++----- .../files/node/watcher/baseWatcher.ts | 4 +- .../node/watcher/parcel/parcelWatcher.ts | 30 ++++----- .../node/nodejsWatcher.integrationTest.ts | 2 +- .../node/parcelWatcher.integrationTest.ts | 2 +- 5 files changed, 70 insertions(+), 34 deletions(-) diff --git a/src/vs/platform/files/common/watcher.ts b/src/vs/platform/files/common/watcher.ts index a81954da0f7..3d5d2e53c87 100644 --- a/src/vs/platform/files/common/watcher.ts +++ b/src/vs/platform/files/common/watcher.ts @@ -5,6 +5,7 @@ import { Event } from 'vs/base/common/event'; import { GLOBSTAR, IRelativePattern, parse, ParsedPattern } from 'vs/base/common/glob'; +import { hash } from 'vs/base/common/hash'; import { Disposable, DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { isAbsolute } from 'vs/base/common/path'; import { isLinux } from 'vs/base/common/platform'; @@ -88,6 +89,11 @@ export function isRecursiveWatchRequest(request: IWatchRequest): request is IRec export type IUniversalWatchRequest = IRecursiveWatchRequest | INonRecursiveWatchRequest; +export interface IWatcherErrorEvent { + readonly error: string; + readonly request?: IUniversalWatchRequest; +} + export interface IWatcher { /** @@ -106,7 +112,7 @@ export interface IWatcher { * that is unrecoverable. Listeners should restart the * watcher if possible. */ - readonly onDidError: Event; + readonly onDidError: Event; /** * Configures the watcher to watch according to the @@ -176,22 +182,24 @@ export interface IUniversalWatcher extends IWatcher { export abstract class AbstractWatcherClient extends Disposable { - private static readonly MAX_RESTARTS = 5; + private static readonly MAX_RESTARTS_PER_REQUEST_ERROR = 3; // how often we give a request a chance to restart on error + private static readonly MAX_RESTARTS_PER_UNKNOWN_ERROR = 10; // how often we give the watcher a chance to restart on unknown errors (like crash) private watcher: IWatcher | undefined; private readonly watcherDisposables = this._register(new MutableDisposable()); private requests: IWatchRequest[] | undefined = undefined; - private restartCounter = 0; + private restartsPerRequestError = new Map(); + private restartsPerUnknownError = 0; constructor( private readonly onFileChanges: (changes: IFileChange[]) => void, private readonly onLogMessage: (msg: ILogMessage) => void, private verboseLogging: boolean, private options: { - type: string; - restartOnError: boolean; + readonly type: string; + readonly restartOnError: boolean; } ) { super(); @@ -212,18 +220,37 @@ export abstract class AbstractWatcherClient extends Disposable { // Wire in event handlers disposables.add(this.watcher.onDidChangeFile(changes => this.onFileChanges(changes))); disposables.add(this.watcher.onDidLogMessage(msg => this.onLogMessage(msg))); - disposables.add(this.watcher.onDidError(error => this.onError(error))); + disposables.add(this.watcher.onDidError(e => this.onError(e.error, e.request))); } - protected onError(error: string): void { + protected onError(error: string, request?: IUniversalWatchRequest): void { // Restart on error (up to N times, if enabled) - if (this.options.restartOnError) { - if (this.restartCounter < AbstractWatcherClient.MAX_RESTARTS && this.requests) { - this.error(`restarting watcher after error: ${error}`); - this.restart(this.requests); - } else { - this.error(`gave up attempting to restart watcher after error: ${error}`); + if (this.options.restartOnError && this.requests?.length) { + + // A request failed + if (request) { + const requestToFilterHash = this.hashRequest(request); + const restartsPerRequestError = this.restartsPerRequestError.get(requestToFilterHash) ?? 0; + if (restartsPerRequestError < AbstractWatcherClient.MAX_RESTARTS_PER_REQUEST_ERROR) { + this.error(`restarting watcher from error in watch request (retrying request): ${error} (${JSON.stringify(request)})`); + this.restartsPerRequestError.set(requestToFilterHash, restartsPerRequestError + 1); + this.restart(this.requests); + } else { + this.error(`restarting watcher from error in watch request (skipping request): ${error} (${JSON.stringify(request)})`); + this.restart(this.requests.filter(request => this.hashRequest(request) !== requestToFilterHash)); + } + } + + // Any request failed or process crashed + else { + if (this.restartsPerUnknownError < AbstractWatcherClient.MAX_RESTARTS_PER_UNKNOWN_ERROR) { + this.error(`restarting watcher after unknown global error: ${error}`); + this.restartsPerUnknownError++; + this.restart(this.requests); + } else { + this.error(`giving up attempting to restart watcher after error: ${error}`); + } } } @@ -233,9 +260,18 @@ export abstract class AbstractWatcherClient extends Disposable { } } - private restart(requests: IUniversalWatchRequest[]): void { - this.restartCounter++; + private hashRequest(request: IWatchRequest): number { + return hash({ + correlationId: request.correlationId, + path: request.path, + recursive: request.recursive, + excludes: request.excludes, + includes: request.includes, + filter: request.filter + }); + } + private restart(requests: IUniversalWatchRequest[]): void { this.init(); this.watch(requests); } diff --git a/src/vs/platform/files/node/watcher/baseWatcher.ts b/src/vs/platform/files/node/watcher/baseWatcher.ts index f80ac506d98..3576f20d4f7 100644 --- a/src/vs/platform/files/node/watcher/baseWatcher.ts +++ b/src/vs/platform/files/node/watcher/baseWatcher.ts @@ -5,7 +5,7 @@ import { watchFile, unwatchFile, Stats } from 'fs'; import { Disposable, DisposableMap, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; -import { ILogMessage, IRecursiveWatcherWithSubscribe, IUniversalWatchRequest, IWatchRequestWithCorrelation, IWatcher, isWatchRequestWithCorrelation, requestFilterToString } from 'vs/platform/files/common/watcher'; +import { ILogMessage, IRecursiveWatcherWithSubscribe, IUniversalWatchRequest, IWatchRequestWithCorrelation, IWatcher, IWatcherErrorEvent, isWatchRequestWithCorrelation, requestFilterToString } from 'vs/platform/files/common/watcher'; import { Emitter, Event } from 'vs/base/common/event'; import { FileChangeType, IFileChange } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; @@ -254,7 +254,7 @@ export abstract class BaseWatcher extends Disposable implements IWatcher { protected abstract trace(message: string): void; protected abstract warn(message: string): void; - abstract onDidError: Event; + abstract onDidError: Event; protected verboseLogging = false; diff --git a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts index 1e37e8d4622..afabd7aed12 100644 --- a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts +++ b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts @@ -21,7 +21,7 @@ import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { realcaseSync, realpathSync } from 'vs/base/node/extpath'; import { NodeJSFileWatcherLibrary } from 'vs/platform/files/node/watcher/nodejs/nodejsWatcherLib'; import { FileChangeType, IFileChange } from 'vs/platform/files/common/files'; -import { coalesceEvents, IRecursiveWatchRequest, parseWatcherPatterns, IRecursiveWatcherWithSubscribe, isFiltered } from 'vs/platform/files/common/watcher'; +import { coalesceEvents, IRecursiveWatchRequest, parseWatcherPatterns, IRecursiveWatcherWithSubscribe, isFiltered, IWatcherErrorEvent } from 'vs/platform/files/common/watcher'; import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; export class ParcelWatcherInstance extends Disposable { @@ -147,7 +147,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS private static readonly PARCEL_WATCHER_BACKEND = isWindows ? 'windows' : isLinux ? 'inotify' : 'fs-events'; - private readonly _onDidError = this._register(new Emitter()); + private readonly _onDidError = this._register(new Emitter()); readonly onDidError = this._onDidError.event; readonly watchers = new Set(); @@ -359,7 +359,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS // the state of parcel at this point and as such will try to restart // up to our maximum of restarts. if (error) { - this.onUnexpectedError(error, watcher); + this.onUnexpectedError(error, request); } // Handle & emit events @@ -372,7 +372,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS instance.complete(parcelWatcher); }).catch(error => { - this.onUnexpectedError(error, watcher); + this.onUnexpectedError(error, request); instance.complete(undefined); @@ -607,7 +607,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS return false; } - private onUnexpectedError(error: unknown, watcher?: ParcelWatcherInstance): void { + private onUnexpectedError(error: unknown, request?: IRecursiveWatchRequest): void { const msg = toErrorMessage(error); // Specially handle ENOSPC errors that can happen when @@ -617,7 +617,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS // See https://github.com/microsoft/vscode/issues/7950 if (msg.indexOf('No space left on device') !== -1) { if (!this.enospcErrorLogged) { - this.error('Inotify limit reached (ENOSPC)', watcher); + this.error('Inotify limit reached (ENOSPC)', request); this.enospcErrorLogged = true; } @@ -627,9 +627,9 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS // restart the watcher as a result to get into healthy // state again if possible and if not attempted too much else { - this.error(`Unexpected error: ${msg} (EUNKNOWN)`, watcher); + this.error(`Unexpected error: ${msg} (EUNKNOWN)`, request); - this._onDidError.fire(msg); + this._onDidError.fire({ request, error: msg }); } } @@ -681,7 +681,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS try { await watcher.stop(joinRestart); } catch (error) { - this.error(`Unexpected error stopping watcher: ${toErrorMessage(error)}`, watcher); + this.error(`Unexpected error stopping watcher: ${toErrorMessage(error)}`, watcher.request); } } @@ -820,20 +820,20 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS protected trace(message: string, watcher?: ParcelWatcherInstance): void { if (this.verboseLogging) { - this._onDidLogMessage.fire({ type: 'trace', message: this.toMessage(message, watcher) }); + this._onDidLogMessage.fire({ type: 'trace', message: this.toMessage(message, watcher?.request) }); } } protected warn(message: string, watcher?: ParcelWatcherInstance) { - this._onDidLogMessage.fire({ type: 'warn', message: this.toMessage(message, watcher) }); + this._onDidLogMessage.fire({ type: 'warn', message: this.toMessage(message, watcher?.request) }); } - private error(message: string, watcher?: ParcelWatcherInstance) { - this._onDidLogMessage.fire({ type: 'error', message: this.toMessage(message, watcher) }); + private error(message: string, request?: IRecursiveWatchRequest) { + this._onDidLogMessage.fire({ type: 'error', message: this.toMessage(message, request) }); } - private toMessage(message: string, watcher?: ParcelWatcherInstance): string { - return watcher ? `[File Watcher (parcel)] ${message} (path: ${watcher.request.path})` : `[File Watcher (parcel)] ${message}`; + private toMessage(message: string, request?: IRecursiveWatchRequest): string { + return request ? `[File Watcher (parcel)] ${message} (path: ${request.path})` : `[File Watcher (parcel)] ${message}`; } protected get recursiveWatcher() { return this; } diff --git a/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts b/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts index 2e585612589..836b67ed9d2 100644 --- a/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts +++ b/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts @@ -701,7 +701,7 @@ import { TestParcelWatcher } from 'vs/platform/files/test/node/parcelWatcher.int recursiveWatcher.onDidError(e => { if (loggingEnabled) { - console.log(`[recursive watcher test error] ${e}`); + console.log(`[recursive watcher test error] ${e.error}`); } }); diff --git a/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts b/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts index 741b64d92c2..db85e28f341 100644 --- a/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts +++ b/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts @@ -87,7 +87,7 @@ export class TestParcelWatcher extends ParcelWatcher { watcher.onDidError(e => { if (loggingEnabled) { - console.log(`[recursive watcher test error] ${e}`); + console.log(`[recursive watcher test error] ${e.error}`); } }); From 577eca457b3dd3e96862653664b2b8afa75861ce Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 17 May 2024 09:05:40 +0200 Subject: [PATCH 253/357] refactor: use request path instead of hashed request for error handling in watcher (#212948) --- src/vs/platform/files/common/watcher.ts | 29 +++++++------------------ 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/src/vs/platform/files/common/watcher.ts b/src/vs/platform/files/common/watcher.ts index 3d5d2e53c87..05ff156de89 100644 --- a/src/vs/platform/files/common/watcher.ts +++ b/src/vs/platform/files/common/watcher.ts @@ -5,7 +5,6 @@ import { Event } from 'vs/base/common/event'; import { GLOBSTAR, IRelativePattern, parse, ParsedPattern } from 'vs/base/common/glob'; -import { hash } from 'vs/base/common/hash'; import { Disposable, DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { isAbsolute } from 'vs/base/common/path'; import { isLinux } from 'vs/base/common/platform'; @@ -190,7 +189,7 @@ export abstract class AbstractWatcherClient extends Disposable { private requests: IWatchRequest[] | undefined = undefined; - private restartsPerRequestError = new Map(); + private restartsPerRequestError = new Map(); private restartsPerUnknownError = 0; constructor( @@ -223,22 +222,21 @@ export abstract class AbstractWatcherClient extends Disposable { disposables.add(this.watcher.onDidError(e => this.onError(e.error, e.request))); } - protected onError(error: string, request?: IUniversalWatchRequest): void { + protected onError(error: string, failedRequest?: IUniversalWatchRequest): void { // Restart on error (up to N times, if enabled) if (this.options.restartOnError && this.requests?.length) { // A request failed - if (request) { - const requestToFilterHash = this.hashRequest(request); - const restartsPerRequestError = this.restartsPerRequestError.get(requestToFilterHash) ?? 0; + if (failedRequest) { + const restartsPerRequestError = this.restartsPerRequestError.get(failedRequest.path) ?? 0; if (restartsPerRequestError < AbstractWatcherClient.MAX_RESTARTS_PER_REQUEST_ERROR) { - this.error(`restarting watcher from error in watch request (retrying request): ${error} (${JSON.stringify(request)})`); - this.restartsPerRequestError.set(requestToFilterHash, restartsPerRequestError + 1); + this.error(`restarting watcher from error in watch request (retrying request): ${error} (${JSON.stringify(failedRequest)})`); + this.restartsPerRequestError.set(failedRequest.path, restartsPerRequestError + 1); this.restart(this.requests); } else { - this.error(`restarting watcher from error in watch request (skipping request): ${error} (${JSON.stringify(request)})`); - this.restart(this.requests.filter(request => this.hashRequest(request) !== requestToFilterHash)); + this.error(`restarting watcher from error in watch request (skipping request): ${error} (${JSON.stringify(failedRequest)})`); + this.restart(this.requests.filter(request => request.path !== failedRequest.path)); } } @@ -260,17 +258,6 @@ export abstract class AbstractWatcherClient extends Disposable { } } - private hashRequest(request: IWatchRequest): number { - return hash({ - correlationId: request.correlationId, - path: request.path, - recursive: request.recursive, - excludes: request.excludes, - includes: request.includes, - filter: request.filter - }); - } - private restart(requests: IUniversalWatchRequest[]): void { this.init(); this.watch(requests); From 8a5f33246183e590ea46e9144d9980ad5af439d0 Mon Sep 17 00:00:00 2001 From: John Murray Date: Fri, 17 May 2024 15:00:00 +0100 Subject: [PATCH 254/357] Revive `TimelineChangeEvent.uri` if passed in `TimelineProvider.onDidChange` event (#212927) --- src/vs/workbench/contrib/timeline/browser/timelinePane.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts index 606c0ed8582..f152f086e9d 100644 --- a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts +++ b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts @@ -418,7 +418,7 @@ export class TimelinePane extends ViewPane { } private onTimelineChanged(e: TimelineChangeEvent) { - if (e?.uri === undefined || this.uriIdentityService.extUri.isEqual(e.uri, this.uri)) { + if (e?.uri === undefined || this.uriIdentityService.extUri.isEqual(URI.revive(e.uri), this.uri)) { const timeline = this.timelinesBySource.get(e.id); if (timeline === undefined) { return; From afee94319da81972a3b5ef449331b6813b7815cc Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Fri, 17 May 2024 17:26:12 +0200 Subject: [PATCH 255/357] Optionally accept previous request entries (microsoft/vscode-copilot#5006) --- .../contrib/chat/browser/actions/chatActions.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 03cd71f82e2..fa695e9d757 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -47,6 +47,15 @@ export interface IChatViewOpenOptions { * Whether the query is partial and will await more input from the user. */ isPartialQuery?: boolean; + /** + * Any previous chat requests and responses that should be shown in the chat view. + */ + previousRequests?: IChatViewOpenRequestEntry[]; +} + +export interface IChatViewOpenRequestEntry { + request: string; + response: string; } class OpenChatGlobalAction extends Action2 { @@ -70,10 +79,16 @@ class OpenChatGlobalAction extends Action2 { override async run(accessor: ServicesAccessor, opts?: string | IChatViewOpenOptions): Promise { opts = typeof opts === 'string' ? { query: opts } : opts; + const chatService = accessor.get(IChatService); const chatWidget = await showChatView(accessor.get(IViewsService)); if (!chatWidget) { return; } + if (opts?.previousRequests?.length && chatWidget.viewModel) { + for (const { request, response } of opts.previousRequests) { + chatService.addCompleteRequest(chatWidget.viewModel.sessionId, request, undefined, 0, { message: response }); + } + } if (opts?.query) { if (opts.isPartialQuery) { chatWidget.setInput(opts.query); From d92efa7d57181aab2cc0161cf1acb793ef22a47a Mon Sep 17 00:00:00 2001 From: isidorn Date: Fri, 17 May 2024 08:49:31 -0700 Subject: [PATCH 256/357] update comment example to use response.text --- src/vscode-dts/vscode.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index e33496f4bfe..e80d251a7fe 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -19008,7 +19008,7 @@ declare module 'vscode' { * ```ts * try { * // consume stream - * for await (const chunk of response.stream) { + * for await (const chunk of response.text) { * console.log(chunk); * } * From f209ce35ef894bd32c12057724e8d1f1139c433f Mon Sep 17 00:00:00 2001 From: SteVen Batten Date: Fri, 17 May 2024 09:20:55 -0700 Subject: [PATCH 257/357] Improve model access dialog messaging (#212974) --- .../api/browser/mainThreadAuthentication.ts | 21 ++++++++++++------- .../api/common/extHostLanguageModels.ts | 4 ++-- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadAuthentication.ts b/src/vs/workbench/api/browser/mainThreadAuthentication.ts index b3ebdd940c3..3719825841a 100644 --- a/src/vs/workbench/api/browser/mainThreadAuthentication.ts +++ b/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -6,7 +6,7 @@ import { Disposable, DisposableMap } from 'vs/base/common/lifecycle'; import * as nls from 'vs/nls'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; -import { IAuthenticationCreateSessionOptions, AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationService, IAuthenticationExtensionsService } from 'vs/workbench/services/authentication/common/authentication'; +import { IAuthenticationCreateSessionOptions, AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationService, IAuthenticationExtensionsService, INTERNAL_AUTH_PROVIDER_PREFIX as INTERNAL_MODEL_AUTH_PROVIDER_PREFIX } from 'vs/workbench/services/authentication/common/authentication'; import { ExtHostAuthenticationShape, ExtHostContext, MainContext, MainThreadAuthenticationShape } from '../common/extHost.protocol'; import { IDialogService, IPromptButton } from 'vs/platform/dialogs/common/dialogs'; import Severity from 'vs/base/common/severity'; @@ -117,10 +117,17 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu $removeSession(providerId: string, sessionId: string): Promise { return this.authenticationService.removeSession(providerId, sessionId); } - private async loginPrompt(providerName: string, extensionName: string, recreatingSession: boolean, options?: AuthenticationForceNewSessionOptions): Promise { - const message = recreatingSession - ? nls.localize('confirmRelogin', "The extension '{0}' wants you to sign in again using {1}.", extensionName, providerName) - : nls.localize('confirmLogin', "The extension '{0}' wants to sign in using {1}.", extensionName, providerName); + private async loginPrompt(provider: IAuthenticationProvider, extensionName: string, recreatingSession: boolean, options?: AuthenticationForceNewSessionOptions): Promise { + let message: string; + + // An internal provider is a special case which is for model access only. + if (provider.id.startsWith(INTERNAL_MODEL_AUTH_PROVIDER_PREFIX)) { + message = nls.localize('confirmModelAccess', "The extension '{0}' wants to access the language models provided by {1}.", extensionName, provider.label); + } else { + message = recreatingSession + ? nls.localize('confirmRelogin', "The extension '{0}' wants you to sign in again using {1}.", extensionName, provider.label) + : nls.localize('confirmLogin', "The extension '{0}' wants to sign in using {1}.", extensionName, provider.label); + } const buttons: IPromptButton[] = [ { @@ -134,7 +141,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu buttons.push({ label: nls.localize('learnMore', "Learn more"), run: async () => { - const result = this.loginPrompt(providerName, extensionName, recreatingSession, options); + const result = this.loginPrompt(provider, extensionName, recreatingSession, options); await this.openerService.open(URI.revive(options.learnMore!), { allowCommands: true }); return await result; } @@ -199,7 +206,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu // We only want to show the "recreating session" prompt if we are using forceNewSession & there are sessions // that we will be "forcing through". const recreatingSession = !!(options.forceNewSession && sessions.length); - const isAllowed = await this.loginPrompt(provider.label, extensionName, recreatingSession, uiOptions); + const isAllowed = await this.loginPrompt(provider, extensionName, recreatingSession, uiOptions); if (!isAllowed) { throw new Error('User did not consent to login.'); } diff --git a/src/vs/workbench/api/common/extHostLanguageModels.ts b/src/vs/workbench/api/common/extHostLanguageModels.ts index 09336569a75..225a0fba95b 100644 --- a/src/vs/workbench/api/common/extHostLanguageModels.ts +++ b/src/vs/workbench/api/common/extHostLanguageModels.ts @@ -382,8 +382,8 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { try { const detail = justification - ? localize('chatAccessWithJustification', "To allow access to the language models provided by {0}. Justification:\n\n{1}", to.displayName, justification) - : localize('chatAccess', "To allow access to the language models provided by {0}", to.displayName); + ? localize('chatAccessWithJustification', "Justification: {1}", to.displayName, justification) + : undefined; await this._extHostAuthentication.getSession(from, providerId, [], { forceNewSession: { detail } }); this.$updateModelAccesslist([{ from: from.identifier, to: to.identifier, enabled: true }]); return true; From ade3d9fe6cab89030d02491530806169abb96e77 Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Fri, 17 May 2024 12:19:41 -0700 Subject: [PATCH 258/357] Rerender notebook cell list when whitespace changes. --- .../notebook/browser/view/notebookCellListView.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellListView.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellListView.ts index 399934d7c49..df7fe89c2b4 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellListView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellListView.ts @@ -290,8 +290,19 @@ export class NotebookCellListView extends ListView { } changeOneWhitespace(id: string, newAfterPosition: number, newSize: number) { - this.notebookRangeMap.changeOneWhitespace(id, newAfterPosition, newSize); - this.eventuallyUpdateScrollDimensions(); + const scrollTop = this.scrollTop; + const previousRenderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight); + const currentPosition = this.notebookRangeMap.getWhitespacePosition(id); + + if (currentPosition > scrollTop) { + this.notebookRangeMap.changeOneWhitespace(id, newAfterPosition, newSize); + this.render(previousRenderRange, scrollTop, this.lastRenderHeight, undefined, undefined, false); + this._rerender(scrollTop, this.renderHeight, false); + this.eventuallyUpdateScrollDimensions(); + } else { + this.notebookRangeMap.changeOneWhitespace(id, newAfterPosition, newSize); + this.eventuallyUpdateScrollDimensions(); + } } removeWhitespace(id: string): void { From 2d663eb25565777f7cbd77cd5c0a3144649ba5a3 Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Fri, 17 May 2024 12:47:11 -0700 Subject: [PATCH 259/357] Fix whitespace positioning after cell 1. --- .../notebook/browser/view/notebookCellList.ts | 7 +++ .../browser/view/notebookCellListView.ts | 2 +- .../test/browser/notebookViewZones.test.ts | 52 +++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index 48910a20dba..a7cd34ca453 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -306,6 +306,13 @@ export class NotebookCellList extends WorkbenchList implements ID return listView; } + /** + * Test Only + */ + _getView() { + return this.view; + } + attachWebview(element: HTMLElement) { element.style.top = `-${NOTEBOOK_WEBVIEW_BOUNDARY}px`; this.rowsContainer.insertAdjacentElement('afterbegin', element); diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellListView.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellListView.ts index df7fe89c2b4..52fbff1055d 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellListView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellListView.ts @@ -189,7 +189,7 @@ export class NotebookCellsLayout implements IRangeMap { const index = afterPosition - 1; const previousItemPosition = this._prefixSumComputer.getPrefixSum(index); const previousItemSize = this._items[index].size; - const previousWhitespace = this._whitespace.filter(ws => ws.afterPosition === afterPosition - 1); + const previousWhitespace = this._whitespace.filter(ws => (ws.afterPosition <= afterPosition - 1 && ws.afterPosition > 0)); const whitespaceBefore = previousWhitespace.reduce((acc, ws) => acc + ws.size, 0); return previousItemPosition + previousItemSize + whitespaceBeforeFirstItem + this.paddingTop + whitespaceBefore; } diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookViewZones.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookViewZones.test.ts index bace7743e88..f2af8c32747 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookViewZones.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookViewZones.test.ts @@ -511,6 +511,58 @@ suite('NotebookRangeMap with whitesspaces', () => { }); }); + test('Multiple Whitespaces 2', async function () { + await withTestNotebook( + [ + ['# header a', 'markdown', CellKind.Markup, [], {}], + ['var b = 1;', 'javascript', CellKind.Code, [], {}], + ['# header b', 'markdown', CellKind.Markup, [], {}], + ['var b = 2;', 'javascript', CellKind.Code, [], {}], + ['# header c', 'markdown', CellKind.Markup, [], {}] + ], + async (editor, viewModel, disposables) => { + viewModel.restoreEditorViewState({ + editingCells: [false, false, false, false, false], + cellLineNumberStates: {}, + editorViewStates: [null, null, null, null, null], + cellTotalHeights: [50, 100, 50, 100, 50], + collapsedInputCells: {}, + collapsedOutputCells: {}, + }); + + const cellList = createNotebookCellList(instantiationService, disposables); + disposables.add(cellList); + cellList.attachViewModel(viewModel); + + // render height 210, it can render 3 full cells and 1 partial cell + cellList.layout(210, 100); + assert.strictEqual(cellList.scrollHeight, 350); + + cellList.changeViewZones(accessor => { + const first = accessor.addZone({ + afterModelPosition: 0, + heightInPx: 20, + domNode: document.createElement('div') + }); + accessor.layoutZone(first); + + const second = accessor.addZone({ + afterModelPosition: 1, + heightInPx: 20, + domNode: document.createElement('div') + }); + accessor.layoutZone(second); + + assert.strictEqual(cellList.scrollHeight, 390); + assert.strictEqual(cellList._getView().getWhitespacePosition(first), 0); + assert.strictEqual(cellList._getView().getWhitespacePosition(second), 70); + + accessor.removeZone(first); + accessor.removeZone(second); + }); + }); + }); + test('Whitespace with folding support', async function () { await withTestNotebook( [ From 1e0404eac6e751165c5f96de57587c3deaabcb6d Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Fri, 17 May 2024 12:48:17 -0700 Subject: [PATCH 260/357] Enable chat agents for notebook. --- .../contrib/chat/browser/contrib/chatInputCompletions.ts | 4 ++-- .../browser/controller/chat/notebookChatController.ts | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts index 5e0dc593588..c94042618c9 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts @@ -39,7 +39,7 @@ class SlashCommandCompletions extends Disposable { triggerCharacters: ['/'], provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); - if (!widget || !widget.viewModel || widget.location !== ChatAgentLocation.Panel /* TODO@jrieken - enable when agents are adopted*/) { + if (!widget || !widget.viewModel || (widget.location !== ChatAgentLocation.Panel && widget.location !== ChatAgentLocation.Notebook) /* TODO@jrieken - enable when agents are adopted*/) { return null; } @@ -95,7 +95,7 @@ class AgentCompletions extends Disposable { triggerCharacters: ['@'], provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); - if (!widget || !widget.viewModel || widget.location !== ChatAgentLocation.Panel /* TODO@jrieken - enable when agents are adopted*/) { + if (!widget || !widget.viewModel || (widget.location !== ChatAgentLocation.Panel && widget.location !== ChatAgentLocation.Notebook) /* TODO@jrieken - enable when agents are adopted*/) { return null; } diff --git a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts index a2ac8fcf663..5e23e2e8b5f 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts @@ -96,7 +96,6 @@ class NotebookChatWidget extends Disposable implements INotebookViewZone { })); this._register(inlineChatWidget.chatWidget.onDidChangeHeight(() => { - console.log('chat widget height changed', inlineChatWidget.chatWidget.contentHeight); updateHeight(); })); @@ -639,10 +638,12 @@ export class NotebookChatController extends Disposable implements INotebookEdito this._widget.inlineChatWidget.addToHistory(lastInput); completeResponseCreated(); + + const agentId = this._widget.inlineChatWidget.chatWidget.lastSelectedAgent ? this._widget.inlineChatWidget.chatWidget.lastSelectedAgent.id : this._notebookDefaultAgentId!; const requestProps: IChatAgentRequest = { sessionId: model.sessionId, requestId: this._currentRequest!.id, - agentId: this._notebookDefaultAgentId!, + agentId: agentId, message: lastInput, variables: { variables: [{ @@ -656,7 +657,7 @@ export class NotebookChatController extends Disposable implements INotebookEdito try { this._ctxHasActiveRequest.set(true); - const task = this._chatAgentService.invokeAgent(this._notebookDefaultAgentId!, requestProps, progressCallback, getHistoryEntriesFromModel(model, this._notebookDefaultAgentId!), cancellationToken); + const task = this._chatAgentService.invokeAgent(agentId, requestProps, progressCallback, getHistoryEntriesFromModel(model, agentId), cancellationToken); this._widget.inlineChatWidget.updateChatMessage(undefined); this._widget.inlineChatWidget.updateFollowUps(undefined); this._widget.inlineChatWidget.updateProgress(true); @@ -888,6 +889,7 @@ export class NotebookChatController extends Disposable implements INotebookEdito this._ctxUserDidEdit.set(false); this._sessionCtor?.cancel(); this._sessionCtor = undefined; + this._model.clear(); this._widget?.dispose(); this._widget = undefined; this._widgetDisposableStore.clear(); From dc78718a1e84787af8872c39e4e0c33da9674b0b Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Fri, 17 May 2024 14:02:11 -0700 Subject: [PATCH 261/357] fix: disable symbols in Attach Context picker (#212986) --- .../quickinput/browser/quickAccess.ts | 19 ++++++++++--------- .../platform/quickinput/common/quickAccess.ts | 13 +++++++++++++ .../browser/actions/chatContextActions.ts | 6 +++++- .../search/browser/anythingQuickAccess.ts | 16 ++++++++-------- 4 files changed, 36 insertions(+), 18 deletions(-) diff --git a/src/vs/platform/quickinput/browser/quickAccess.ts b/src/vs/platform/quickinput/browser/quickAccess.ts index 88169c32ed5..7cccc352393 100644 --- a/src/vs/platform/quickinput/browser/quickAccess.ts +++ b/src/vs/platform/quickinput/browser/quickAccess.ts @@ -8,7 +8,7 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { DefaultQuickAccessFilterValue, Extensions, IQuickAccessController, IQuickAccessOptions, IQuickAccessProvider, IQuickAccessProviderDescriptor, IQuickAccessProviderRunOptions, IQuickAccessRegistry } from 'vs/platform/quickinput/common/quickAccess'; +import { DefaultQuickAccessFilterValue, Extensions, IQuickAccessController, IQuickAccessOptions, IQuickAccessProvider, IQuickAccessProviderDescriptor, IQuickAccessRegistry } from 'vs/platform/quickinput/common/quickAccess'; import { IQuickInputService, IQuickPick, IQuickPickItem, ItemActivation } from 'vs/platform/quickinput/common/quickInput'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -45,7 +45,7 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon private doShowOrPick(value: string, pick: boolean, options?: IQuickAccessOptions): Promise | void { // Find provider for the value to show - const [provider, descriptor] = this.getOrInstantiateProvider(value); + const [provider, descriptor] = this.getOrInstantiateProvider(value, options?.enabledProviderPrefixes); // Return early if quick access is already showing on that same prefix const visibleQuickAccess = this.visibleQuickAccess; @@ -102,7 +102,7 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon const picker = disposables.add(this.quickInputService.createQuickPick()); picker.value = value; this.adjustValueSelection(picker, descriptor, options); - picker.placeholder = descriptor?.placeholder; + picker.placeholder = options?.placeholder ?? descriptor?.placeholder; picker.quickNavigate = options?.quickNavigateConfiguration; picker.hideInput = !!picker.quickNavigate && !visibleQuickAccess; // only hide input if there was no picker opened already if (typeof options?.itemActivation === 'number' || options?.quickNavigateConfiguration) { @@ -123,7 +123,7 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon } // Register listeners - disposables.add(this.registerPickerListeners(picker, provider, descriptor, value, options?.providerOptions)); + disposables.add(this.registerPickerListeners(picker, provider, descriptor, value, options)); // Ask provider to fill the picker as needed if we have one // and pass over a cancellation token that will indicate when @@ -184,7 +184,7 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon provider: IQuickAccessProvider | undefined, descriptor: IQuickAccessProviderDescriptor | undefined, value: string, - providerOptions?: IQuickAccessProviderRunOptions + options?: IQuickAccessOptions ): IDisposable { const disposables = new DisposableStore(); @@ -199,13 +199,14 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon // Whenever the value changes, check if the provider has // changed and if so - re-create the picker from the beginning disposables.add(picker.onDidChangeValue(value => { - const [providerForValue] = this.getOrInstantiateProvider(value); + const [providerForValue] = this.getOrInstantiateProvider(value, options?.enabledProviderPrefixes); if (providerForValue !== provider) { this.show(value, { + enabledProviderPrefixes: options?.enabledProviderPrefixes, // do not rewrite value from user typing! preserveValue: true, // persist the value of the providerOptions from the original showing - providerOptions + providerOptions: options?.providerOptions }); } else { visibleQuickAccess.value = value; // remember the value in our visible one @@ -222,9 +223,9 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon return disposables; } - private getOrInstantiateProvider(value: string): [IQuickAccessProvider | undefined, IQuickAccessProviderDescriptor | undefined] { + private getOrInstantiateProvider(value: string, enabledProviderPrefixes?: string[]): [IQuickAccessProvider | undefined, IQuickAccessProviderDescriptor | undefined] { const providerDescriptor = this.registry.getQuickAccessProvider(value); - if (!providerDescriptor) { + if (!providerDescriptor || enabledProviderPrefixes && !enabledProviderPrefixes?.includes(providerDescriptor.prefix)) { return [undefined, undefined]; } diff --git a/src/vs/platform/quickinput/common/quickAccess.ts b/src/vs/platform/quickinput/common/quickAccess.ts index a8256454a91..23c80d0246d 100644 --- a/src/vs/platform/quickinput/common/quickAccess.ts +++ b/src/vs/platform/quickinput/common/quickAccess.ts @@ -15,6 +15,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; */ export interface IQuickAccessProviderRunOptions { readonly from?: string; + readonly placeholder?: string; } /** @@ -23,6 +24,7 @@ export interface IQuickAccessProviderRunOptions { export interface AnythingQuickAccessProviderRunOptions extends IQuickAccessProviderRunOptions { readonly includeHelp?: boolean; readonly filter?: (item: unknown) => boolean; + readonly includeSymbols?: boolean; /** * @deprecated - temporary for Dynamic Chat Variables (see usage) until it has built-in UX for file picking * Useful for adding items to the top of the list that might contain actions. @@ -54,6 +56,17 @@ export interface IQuickAccessOptions { * quick access. */ readonly providerOptions?: IQuickAccessProviderRunOptions; + + /** + * An array of provider prefixes to enable for this + * particular showing of the quick access. + */ + readonly enabledProviderPrefixes?: string[]; + + /** + * A placeholder to use for this particular showing of the quick access. + */ + readonly placeholder?: string; } export interface IQuickAccessController { diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index 4b773fd2c57..f9e94661e10 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -8,7 +8,7 @@ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; -import { localize2 } from 'vs/nls'; +import { localize, localize2 } from 'vs/nls'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { AnythingQuickAccessProviderRunOptions } from 'vs/platform/quickinput/common/quickAccess'; @@ -19,6 +19,7 @@ import { SelectAndInsertFileAction } from 'vs/workbench/contrib/chat/browser/con import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; import { CONTEXT_CHAT_LOCATION, CONTEXT_IN_CHAT_INPUT } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; +import { AnythingQuickAccessProvider } from 'vs/workbench/contrib/search/browser/anythingQuickAccess'; export function registerChatContextActions() { registerAction2(AttachContextAction); @@ -65,8 +66,11 @@ class AttachContextAction extends Action2 { } const picks = await quickInputService.quickAccess.pick('', { + enabledProviderPrefixes: [AnythingQuickAccessProvider.PREFIX], + placeholder: localize('chatContext.searchFiles.placeholder', 'Search files by name'), providerOptions: { additionPicks: quickPickItems, + includeSymbols: false, filter: (item) => { if (item && typeof item === 'object' && 'resource' in item && URI.isUri(item.resource)) { return [Schemas.file, Schemas.vscodeRemote].includes(item.resource.scheme); diff --git a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts index 66badbec7ae..bdf461d00de 100644 --- a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts @@ -319,12 +319,13 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider | Promise> | FastAndSlowPicks { + const configuration = { ...this.configuration, includeSymbols: options.includeSymbols ?? this.configuration.includeSymbols }; const query = prepareQuery(filter); // Return early if we have editor symbol picks. We support this by: // - having a previously active global pick (e.g. a file) // - the user typing `@` to start the local symbol query - if (options.enableEditorSymbolSearch) { + if (options.enableEditorSymbolSearch && options.includeSymbols) { const editorSymbolPicks = this.getEditorSymbolPicks(query, disposables, token); if (editorSymbolPicks) { return editorSymbolPicks; @@ -377,7 +378,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider options.filter?.(p)); } @@ -386,7 +387,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider 0 ? [ - { type: 'separator', label: this.configuration.includeSymbols ? localize('fileAndSymbolResultsSeparator', "file and symbol results") : localize('fileResultsSeparator', "file results") }, + { type: 'separator', label: configuration.includeSymbols ? localize('fileAndSymbolResultsSeparator', "file and symbol results") : localize('fileResultsSeparator', "file results") }, ...additionalPicks ] : []; })(), @@ -396,12 +397,12 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider, token: CancellationToken): Promise> { + private async getAdditionalPicks(query: IPreparedQuery, excludes: ResourceMap, includeSymbols: boolean, token: CancellationToken): Promise> { // Resolve file and symbol picks (if enabled) const [filePicks, symbolPicks] = await Promise.all([ this.getFilePicks(query, excludes, token), - this.getWorkspaceSymbolPicks(query, token) + this.getWorkspaceSymbolPicks(query, includeSymbols, token) ]); if (token.isCancellationRequested) { @@ -809,11 +810,10 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider> { - const configuration = this.configuration; + private async getWorkspaceSymbolPicks(query: IPreparedQuery, includeSymbols: boolean, token: CancellationToken): Promise> { if ( !query.normalized || // we need a value for search for - !configuration.includeSymbols || // we need to enable symbols in search + !includeSymbols || // we need to enable symbols in search this.pickState.lastRange // a range is an indicator for just searching for files ) { return []; From 3a0bb7b3a60f0d4f57d37105886d8ad297876514 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Fri, 17 May 2024 14:16:18 -0700 Subject: [PATCH 262/357] Fix config provider issues during restart (#212991) - Taking the resolved debug type from the previous run may cause us to skip the correct config provider - Need to use the correct resolved type for the call to `resolveDebugConfigurationWithSubstitutedVariables` Fix #212985 --- src/vs/workbench/contrib/debug/browser/debugService.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index 90f55a2868d..d09eada1d2a 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -796,8 +796,6 @@ export class DebugService implements IDebugService { if (launch) { unresolved = launch.getConfiguration(session.configuration.name); if (unresolved && !equals(unresolved, session.unresolvedConfiguration)) { - // Take the type from the session since the debug extension might overwrite it #21316 - unresolved.type = session.configuration.type; unresolved.noDebug = session.configuration.noDebug; needsToSubstitute = true; } @@ -811,7 +809,7 @@ export class DebugService implements IDebugService { if (resolvedByProviders) { resolved = await this.substituteVariables(launch, resolvedByProviders); if (resolved && !initCancellationToken.token.isCancellationRequested) { - resolved = await this.configurationManager.resolveDebugConfigurationWithSubstitutedVariables(launch && launch.workspace ? launch.workspace.uri : undefined, unresolved.type, resolved, initCancellationToken.token); + resolved = await this.configurationManager.resolveDebugConfigurationWithSubstitutedVariables(launch && launch.workspace ? launch.workspace.uri : undefined, resolved.type, resolved, initCancellationToken.token); } } else { resolved = resolvedByProviders; From 8517ac770a16abefa47d5f3a1c84be83ef717aac Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Fri, 17 May 2024 15:34:40 -0700 Subject: [PATCH 263/357] make agent hover keyboard accessible (#212933) --- .../contrib/chat/browser/chatListRenderer.ts | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 0875cb6bfd7..76b491f5848 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -79,6 +79,8 @@ import { IMarkdownVulnerability, annotateSpecialMarkdownContent } from '../commo import { CodeBlockModelCollection } from '../common/codeBlockModelCollection'; import { IChatListItemRendererOptions } from './chat'; import { renderFormattedText } from 'vs/base/browser/formattedTextRenderer'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { KeyCode } from 'vs/base/common/keyCodes'; const $ = dom.$; @@ -267,6 +269,8 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { + const hoverContent = () => { if (isResponseVM(template.currentElement) && template.currentElement.agent) { agentHover.setAgent(template.currentElement.agent.id); return agentHover.domNode; } return undefined; - }, getChatAgentHoverOptions(() => isResponseVM(template.currentElement) ? template.currentElement.agent : undefined, this.commandService))); - + }; + const hoverOptions = getChatAgentHoverOptions(() => isResponseVM(template.currentElement) ? template.currentElement.agent : undefined, this.commandService); + templateDisposables.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('element'), user, hoverContent, hoverOptions)); + templateDisposables.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), header, hoverContent, hoverOptions)); + templateDisposables.add(dom.addDisposableListener(user, dom.EventType.KEY_DOWN, e => { + const ev = new StandardKeyboardEvent(e); + if (ev.equals(KeyCode.Space) || ev.equals(KeyCode.Enter)) { + const content = hoverContent(); + if (content) { + this.hoverService.showHover({ content, target: user, trapFocus: true }, true); + } + } else if (ev.equals(KeyCode.Escape)) { + this.hoverService.hideHover(); + } + })); const template: IChatListItemTemplate = { avatarContainer, username, detail, referencesListContainer, value, rowContainer, elementDisposables, titleToolbar, templateDisposables, contextKeyService, agentHover }; return template; } From 5d6671dacb9d6a582b9354ea317211a8e2b2f918 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Fri, 17 May 2024 16:06:22 -0700 Subject: [PATCH 264/357] feat: support static chat variables in Attach Context picker (#212993) --- .../workbench/api/common/extHost.api.impl.ts | 4 ++-- .../api/common/extHostChatVariables.ts | 8 ++++--- .../browser/actions/chatContextActions.ts | 22 +++++++++++++++---- .../contrib/chat/browser/chatInputPart.ts | 2 +- .../contrib/chat/browser/chatVariables.ts | 22 ++++++++++++++++++- .../contrib/chat/browser/media/chat.css | 8 ++++++- .../contrib/chat/common/chatModel.ts | 2 ++ .../contrib/chat/common/chatServiceImpl.ts | 5 +---- .../contrib/chat/common/chatVariables.ts | 7 ++++-- .../chat/test/browser/chatVariables.test.ts | 2 +- .../chat/test/common/mockChatVariables.ts | 4 ++-- .../vscode.proposed.chatVariableResolver.d.ts | 4 +++- 12 files changed, 68 insertions(+), 22 deletions(-) diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 3b3c6f87b22..04372c16964 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1411,9 +1411,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'chatProvider'); return extHostLanguageModels.registerLanguageModel(extension, id, provider, metadata); }, - registerChatVariableResolver(id: string, name: string, userDescription: string, modelDescription: string | undefined, isSlow: boolean | undefined, resolver: vscode.ChatVariableResolver) { + registerChatVariableResolver(id: string, name: string, userDescription: string, modelDescription: string | undefined, isSlow: boolean | undefined, resolver: vscode.ChatVariableResolver, fullName?: string, icon?: vscode.ThemeIcon) { checkProposedApiEnabled(extension, 'chatVariableResolver'); - return extHostChatVariables.registerVariableResolver(extension, id, name, userDescription, modelDescription, isSlow, resolver); + return extHostChatVariables.registerVariableResolver(extension, id, name, userDescription, modelDescription, isSlow, resolver, fullName, icon?.id); }, registerMappedEditsProvider(selector: vscode.DocumentSelector, provider: vscode.MappedEditsProvider) { checkProposedApiEnabled(extension, 'mappedEditsProvider'); diff --git a/src/vs/workbench/api/common/extHostChatVariables.ts b/src/vs/workbench/api/common/extHostChatVariables.ts index 43a9dcceb53..dfc37201bd4 100644 --- a/src/vs/workbench/api/common/extHostChatVariables.ts +++ b/src/vs/workbench/api/common/extHostChatVariables.ts @@ -6,6 +6,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { onUnexpectedExternalError } from 'vs/base/common/errors'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { ThemeIcon } from 'vs/base/common/themables'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ExtHostChatVariablesShape, IChatVariableResolverProgressDto, IMainContext, MainContext, MainThreadChatVariablesShape } from 'vs/workbench/api/common/extHost.protocol'; import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; @@ -52,10 +53,11 @@ export class ExtHostChatVariables implements ExtHostChatVariablesShape { return undefined; } - registerVariableResolver(extension: IExtensionDescription, id: string, name: string, userDescription: string, modelDescription: string | undefined, isSlow: boolean | undefined, resolver: vscode.ChatVariableResolver): IDisposable { + registerVariableResolver(extension: IExtensionDescription, id: string, name: string, userDescription: string, modelDescription: string | undefined, isSlow: boolean | undefined, resolver: vscode.ChatVariableResolver, fullName?: string, themeIconId?: string): IDisposable { const handle = ExtHostChatVariables._idPool++; - this._resolver.set(handle, { extension, data: { id, name, description: userDescription, modelDescription }, resolver: resolver }); - this._proxy.$registerVariable(handle, { id, name, description: userDescription, modelDescription, isSlow }); + const icon = themeIconId ? ThemeIcon.fromId(themeIconId) : undefined; + this._resolver.set(handle, { extension, data: { id, name, description: userDescription, modelDescription, icon }, resolver: resolver }); + this._proxy.$registerVariable(handle, { id, name, description: userDescription, modelDescription, isSlow, fullName, icon }); return toDisposable(() => { this._resolver.delete(handle); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index f9e94661e10..a6516a796de 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -6,6 +6,7 @@ import { Codicon } from 'vs/base/common/codicons'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Schemas } from 'vs/base/common/network'; +import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { localize, localize2 } from 'vs/nls'; @@ -37,7 +38,7 @@ class AttachContextAction extends Action2 { category: CHAT_CATEGORY, keybinding: { when: CONTEXT_IN_CHAT_INPUT, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyA, + primary: KeyMod.CtrlCmd | KeyCode.Slash, weight: KeybindingWeight.EditorContrib }, menu: [ @@ -60,14 +61,20 @@ class AttachContextAction extends Action2 { const chatVariablesService = accessor.get(IChatVariablesService); const widgetService = accessor.get(IChatWidgetService); - const quickPickItems: QuickPickItem[] = []; + const quickPickItems: (QuickPickItem & { name?: string; icon?: ThemeIcon })[] = []; + for (const variable of chatVariablesService.getVariables()) { + if (variable.fullName) { + quickPickItems.push({ label: `${variable.icon ? `$(${variable.icon.id}) ` : ''}${variable.fullName}`, name: variable.name, id: variable.id, icon: variable.icon }); + } + } + if (chatVariablesService.hasVariable(SelectAndInsertFileAction.Name)) { quickPickItems.push(SelectAndInsertFileAction.Item, { type: 'separator' }); } const picks = await quickInputService.quickAccess.pick('', { enabledProviderPrefixes: [AnythingQuickAccessProvider.PREFIX], - placeholder: localize('chatContext.searchFiles.placeholder', 'Search files by name'), + placeholder: localize('chatContext.attach.placeholder', 'Search attachments'), providerOptions: { additionPicks: quickPickItems, includeSymbols: false, @@ -84,7 +91,14 @@ class AttachContextAction extends Action2 { const context: { widget?: IChatWidget } | undefined = args[0]; const widget = context?.widget ?? widgetService.lastFocusedWidget; - widget?.attachContext(...picks.map((p) => ({ name: p.label, value: 'resource' in p && URI.isUri(p.resource) ? p.resource : undefined, id: 'resource' in p && URI.isUri(p.resource) ? `${SelectAndInsertFileAction.Name}:${p.resource.toString()}` : '' }))); + widget?.attachContext(...picks.map((p) => ({ + fullName: p.label, + icon: 'icon' in p && ThemeIcon.isThemeIcon(p.icon) ? p.icon : undefined, + name: 'name' in p && typeof p.name === 'string' ? p.name : p.label, + value: 'resource' in p && URI.isUri(p.resource) ? p.resource : undefined, + id: 'id' in p && typeof p.id === 'string' ? p.id : + 'resource' in p && URI.isUri(p.resource) ? `${SelectAndInsertFileAction.Name}:${p.resource.toString()}` : '' + }))); } } } diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 5f191e62655..17d6f47866a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -441,7 +441,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge hidePath: true, }); } else { - label.setLabel(attachment.name); + label.setLabel(attachment.fullName ?? attachment.name); } const clearButton = new Button(widget, { supportIcons: true }); diff --git a/src/vs/workbench/contrib/chat/browser/chatVariables.ts b/src/vs/workbench/contrib/chat/browser/chatVariables.ts index 8474766e0e9..560f2f887e2 100644 --- a/src/vs/workbench/contrib/chat/browser/chatVariables.ts +++ b/src/vs/workbench/contrib/chat/browser/chatVariables.ts @@ -30,7 +30,7 @@ export class ChatVariablesService implements IChatVariablesService { ) { } - async resolveVariables(prompt: IParsedChatRequest, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise { + async resolveVariables(prompt: IParsedChatRequest, attachedContextVariables: IChatRequestVariableEntry[] | undefined, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise { let resolvedVariables: IChatRequestVariableEntry[] = []; const jobs: Promise[] = []; @@ -58,6 +58,26 @@ export class ChatVariablesService implements IChatVariablesService { } }); + attachedContextVariables + ?.forEach((attachment, i) => { + const data = this._resolver.get(attachment.name.toLowerCase()); + if (data) { + const references: IChatContentReference[] = []; + const variableProgressCallback = (item: IChatVariableResolverProgress) => { + if (item.kind === 'reference') { + references.push(item); + return; + } + progress(item); + }; + jobs.push(data.resolver(prompt.text, '', model, variableProgressCallback, token).then(value => { + if (value) { + resolvedVariables[i] = { id: data.data.id, modelDescription: data.data.modelDescription, name: attachment.name, range: attachment.range, value, references }; + } + }).catch(onUnexpectedExternalError)); + } + }); + await Promise.allSettled(jobs); resolvedVariables = coalesce(resolvedVariables); diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 5c4aef84471..97c1103e53d 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -489,7 +489,13 @@ align-items: center; } -.interactive-session .chat-attached-context .chat-attached-context-attachment .codicon { +.interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-icon-label-container .monaco-highlighted-label { + display: flex !important; + align-items: center !important; +} + +.interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-icon-label .monaco-button.codicon.codicon-close, +.interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-button.codicon.codicon-close { color: var(--vscode-descriptionForeground); } diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 6fe68717c3e..895c910ed24 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -25,6 +25,8 @@ import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chat export interface IChatRequestVariableEntry { id: string; + fullName?: string; + icon?: ThemeIcon; name: string; modelDescription?: string; range?: IOffsetRange; diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index 8acb4f44e8d..a106ee69435 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -561,10 +561,7 @@ export class ChatService extends Disposable implements IChatService { const initVariableData: IChatRequestVariableData = { variables: [] }; request = model.addRequest(parsedRequest, initVariableData, attempt, agent, agentSlashCommandPart?.command); completeResponseCreated(); - const variableData = await this.chatVariablesService.resolveVariables(parsedRequest, model, progressCallback, token); - if (options?.attachedContext) { - variableData.variables.push(...options.attachedContext); - } + const variableData = await this.chatVariablesService.resolveVariables(parsedRequest, options?.attachedContext, model, progressCallback, token); request.variableData = variableData; const promptTextResult = getPromptText(request.message); diff --git a/src/vs/workbench/contrib/chat/common/chatVariables.ts b/src/vs/workbench/contrib/chat/common/chatVariables.ts index 602b899c56c..644cad67a79 100644 --- a/src/vs/workbench/contrib/chat/common/chatVariables.ts +++ b/src/vs/workbench/contrib/chat/common/chatVariables.ts @@ -5,17 +5,20 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; import { IRange } from 'vs/editor/common/core/range'; import { Location } from 'vs/editor/common/languages'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IChatModel, IChatRequestVariableData } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatModel, IChatRequestVariableData, IChatRequestVariableEntry } from 'vs/workbench/contrib/chat/common/chatModel'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatContentReference, IChatProgressMessage } from 'vs/workbench/contrib/chat/common/chatService'; export interface IChatVariableData { id: string; name: string; + icon?: ThemeIcon; + fullName?: string; description: string; modelDescription?: string; isSlow?: boolean; @@ -46,7 +49,7 @@ export interface IChatVariablesService { /** * Resolves all variables that occur in `prompt` */ - resolveVariables(prompt: IParsedChatRequest, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise; + resolveVariables(prompt: IParsedChatRequest, attachedContextVariables: IChatRequestVariableEntry[] | undefined, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise; resolveVariable(variableName: string, promptText: string, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise; } 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 530752d4118..40f72512a6a 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatVariables.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatVariables.test.ts @@ -48,7 +48,7 @@ suite('ChatVariables', function () { const resolveVariables = async (text: string) => { const result = parser.parseChatRequest('1', text); - return await service.resolveVariables(result, null!, () => { }, CancellationToken.None); + return await service.resolveVariables(result, undefined, null!, () => { }, CancellationToken.None); }; { diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatVariables.ts b/src/vs/workbench/contrib/chat/test/common/mockChatVariables.ts index 902631c259a..d18f9b473df 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatVariables.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatVariables.ts @@ -5,7 +5,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { IDisposable } from 'vs/base/common/lifecycle'; -import { IChatModel, IChatRequestVariableData } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatModel, IChatRequestVariableData, IChatRequestVariableEntry } from 'vs/workbench/contrib/chat/common/chatModel'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatRequestVariableValue, IChatVariableData, IChatVariableResolver, IChatVariableResolverProgress, IChatVariablesService, IDynamicVariable } from 'vs/workbench/contrib/chat/common/chatVariables'; @@ -31,7 +31,7 @@ export class MockChatVariablesService implements IChatVariablesService { return []; } - async resolveVariables(prompt: IParsedChatRequest, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise { + async resolveVariables(prompt: IParsedChatRequest, attachedContextVariables: IChatRequestVariableEntry[] | undefined, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise { return { variables: [] }; diff --git a/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts b/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts index 7e43d3ad546..91380192de2 100644 --- a/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts +++ b/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts @@ -15,8 +15,10 @@ declare module 'vscode' { * @param modelDescription A description of the variable for the model. * @param isSlow Temp, to limit access to '#codebase' which is not a 'reference' and will fit into a tools API later. * @param resolver Will be called to provide the chat variable's value when it is used. + * @param fullName The full name of the variable when selecting context in the picker UI. + * @param icon An icon to display when selecting context in the picker UI. */ - export function registerChatVariableResolver(id: string, name: string, userDescription: string, modelDescription: string | undefined, isSlow: boolean | undefined, resolver: ChatVariableResolver): Disposable; + export function registerChatVariableResolver(id: string, name: string, userDescription: string, modelDescription: string | undefined, isSlow: boolean | undefined, resolver: ChatVariableResolver, fullName?: string, icon?: ThemeIcon): Disposable; } export interface ChatVariableValue { From 9f83563406c8b2022b5101116b00f0c6c7721863 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Sat, 18 May 2024 10:17:38 -0700 Subject: [PATCH 265/357] add configure keybindings quickpick for extension contributed accessibility help dialogs (#212997) --- .../accessibility/browser/accessibleView.ts | 7 ++++ .../api/browser/viewsExtensionPoint.ts | 2 +- .../accessibility/browser/accessibleView.ts | 29 +++++++++++++++-- .../browser/accessibleViewActions.ts | 19 +++++++++++ .../extensionAccesibilityHelp.contribution.ts | 32 ++++++++++++------- .../common/accessibilityCommands.ts | 3 +- 6 files changed, 76 insertions(+), 16 deletions(-) diff --git a/src/vs/platform/accessibility/browser/accessibleView.ts b/src/vs/platform/accessibility/browser/accessibleView.ts index 34d2b431a08..7674bf33f24 100644 --- a/src/vs/platform/accessibility/browser/accessibleView.ts +++ b/src/vs/platform/accessibility/browser/accessibleView.ts @@ -8,6 +8,7 @@ import { IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding'; import { IPickerQuickAccessItem } from 'vs/platform/quickinput/browser/pickerQuickAccess'; import { Event } from 'vs/base/common/event'; import { IAction } from 'vs/base/common/actions'; +import { IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; export const IAccessibleViewService = createDecorator('accessibleViewService'); @@ -60,6 +61,11 @@ export interface IAccessibleViewOptions { * If this provider might want to request to be shown again, provide an ID. */ id?: AccessibleViewProviderId; + + /** + * Keybinding items to configure + */ + configureKeybindingItems?: IQuickPickItem[]; } @@ -113,6 +119,7 @@ export interface IAccessibleViewService { */ getOpenAriaHint(verbositySettingKey: string): string | null; getCodeBlockContext(): ICodeBlockActionContext | undefined; + configureKeybindings(): void; } diff --git a/src/vs/workbench/api/browser/viewsExtensionPoint.ts b/src/vs/workbench/api/browser/viewsExtensionPoint.ts index bab9d8b97e5..73ca041e3db 100644 --- a/src/vs/workbench/api/browser/viewsExtensionPoint.ts +++ b/src/vs/workbench/api/browser/viewsExtensionPoint.ts @@ -173,7 +173,7 @@ const viewDescriptor: IJSONSchema = { }, accessibilityHelpContent: { type: 'string', - markdownDescription: localize('vscode.extension.contributes.view.accessibilityHelpContent', "When the accessibility help dialog is invoked in this view, this content will be presented to the user as a markdown string. Keybindings will be resolved when provided in the format of . If there is no keybinding, that will be indicated with a link to configure one.") + markdownDescription: localize('vscode.extension.contributes.view.accessibilityHelpContent', "When the accessibility help dialog is invoked in this view, this content will be presented to the user as a markdown string. Keybindings will be resolved when provided in the format of . If there is no keybinding, that will be indicated and this command will be included in a quickpick for easy configuration.") } } }; diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts index dbba0da0420..25f23161a5a 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts @@ -38,7 +38,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ResultKind } from 'vs/platform/keybinding/common/keybindingResolver'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { IQuickInputService, IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { AccessibilityVerbositySettingId, AccessibilityWorkbenchSettingId, accessibilityHelpIsShown, accessibleViewContainsCodeBlocks, accessibleViewCurrentProviderId, accessibleViewGoToSymbolSupported, accessibleViewInCodeBlock, accessibleViewIsShown, accessibleViewOnLastLine, accessibleViewSupportsNavigation, accessibleViewVerbosityEnabled } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; @@ -96,7 +96,8 @@ export class AccessibleView extends Disposable { @IMenuService private readonly _menuService: IMenuService, @ICommandService private readonly _commandService: ICommandService, @IChatCodeBlockContextProviderService private readonly _codeBlockContextProviderService: IChatCodeBlockContextProviderService, - @IStorageService private readonly _storageService: IStorageService + @IStorageService private readonly _storageService: IStorageService, + @IQuickInputService private readonly _quickInputService: IQuickInputService ) { super(); @@ -361,6 +362,27 @@ export class AccessibleView extends Disposable { return symbols.length ? symbols : undefined; } + configureKeybindings(): void { + const items = this._currentProvider?.options?.configureKeybindingItems; + if (!items) { + return; + } + const quickPick: IQuickPick = this._quickInputService.createQuickPick(); + this._register(quickPick); + quickPick.items = items; + quickPick.title = localize('keybindings', 'Configure keybindings'); + quickPick.placeholder = localize('selectKeybinding', 'Select a command ID to configure a keybinding for it'); + quickPick.show(); + quickPick.onDidAccept(async () => { + const item = quickPick.selectedItems[0]; + if (item) { + await this._commandService.executeCommand('workbench.action.openGlobalKeybindings', item.id); + } + quickPick.dispose(); + }); + quickPick.onDidHide(() => quickPick.dispose()); + } + private _convertTokensToSymbols(tokens: marked.TokensList, symbols: IAccessibleViewSymbol[]): void { let firstListItem: string | undefined; for (const token of tokens) { @@ -750,6 +772,9 @@ export class AccessibleViewService extends Disposable implements IAccessibleView } this._accessibleView.show(provider, undefined, undefined, position); } + configureKeybindings(): void { + this._accessibleView?.configureKeybindings(); + } showLastProvider(id: AccessibleViewProviderId): void { this._accessibleView?.showLastProvider(id); } diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts index 0284884cab2..8d9bd29623d 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts @@ -228,6 +228,24 @@ class AccessibleViewDisableHintAction extends Action2 { } registerAction2(AccessibleViewDisableHintAction); +class AccessibilityHelpConfigureKeybindingsAction extends Action2 { + constructor() { + super({ + id: AccessibilityCommandId.AccessibilityHelpConfigureKeybindings, + precondition: ContextKeyExpr.and(accessibilityHelpIsShown), + keybinding: { + primary: KeyMod.Alt | KeyCode.KeyK, + weight: KeybindingWeight.WorkbenchContrib + }, + title: localize('editor.action.accessibilityHelpConfigureKeybindings', "Accessibility Help Configure Keybindings") + }); + } + async run(accessor: ServicesAccessor): Promise { + await accessor.get(IAccessibleViewService).configureKeybindings(); + } +} +registerAction2(AccessibilityHelpConfigureKeybindingsAction); + class AccessibleViewAcceptInlineCompletionAction extends Action2 { constructor() { super({ @@ -267,3 +285,4 @@ class AccessibleViewAcceptInlineCompletionAction extends Action2 { } } registerAction2(AccessibleViewAcceptInlineCompletionAction); + diff --git a/src/vs/workbench/contrib/accessibility/browser/extensionAccesibilityHelp.contribution.ts b/src/vs/workbench/contrib/accessibility/browser/extensionAccesibilityHelp.contribution.ts index 6161c11f1cc..2e5056bc567 100644 --- a/src/vs/workbench/contrib/accessibility/browser/extensionAccesibilityHelp.contribution.ts +++ b/src/vs/workbench/contrib/accessibility/browser/extensionAccesibilityHelp.contribution.ts @@ -5,14 +5,15 @@ import { MarkdownString } from 'vs/base/common/htmlContent'; import { DisposableMap, IDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; -import { URI } from 'vs/base/common/uri'; import { AccessibleViewType, ExtensionContentProvider } from 'vs/platform/accessibility/browser/accessibleView'; import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IPickerQuickAccessItem } from 'vs/platform/quickinput/browser/pickerQuickAccess'; import { Registry } from 'vs/platform/registry/common/platform'; import { FocusedViewContext } from 'vs/workbench/common/contextkeys'; import { IViewsRegistry, Extensions, IViewDescriptor } from 'vs/workbench/common/views'; +import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; export class ExtensionAccessibilityHelpDialogContribution extends Disposable { @@ -54,9 +55,9 @@ function registerAccessibilityHelpAction(keybindingService: IKeybindingService, const viewsService = accessor.get(IViewsService); return new ExtensionContentProvider( viewDescriptor.id, - { type: AccessibleViewType.Help }, - () => helpContent.value, - () => viewsService.openView(viewDescriptor.id, true) + { type: AccessibleViewType.Help, configureKeybindingItems: helpContent.configureKeybindingItems }, + () => helpContent.value.value, + () => viewsService.openView(viewDescriptor.id, true), ); } })); @@ -68,27 +69,34 @@ function registerAccessibilityHelpAction(keybindingService: IKeybindingService, return disposableStore; } -function resolveExtensionHelpContent(keybindingService: IKeybindingService, content?: MarkdownString): MarkdownString | undefined { +function resolveExtensionHelpContent(keybindingService: IKeybindingService, content?: MarkdownString): { value: MarkdownString; configureKeybindingItems: IPickerQuickAccessItem[] | undefined } | undefined { if (!content) { return; } + const configureKeybindingItems: IPickerQuickAccessItem[] = []; let resolvedContent = typeof content === 'string' ? content : content.value; const matches = resolvedContent.matchAll(/\.*)\>/gm); for (const match of [...matches]) { const commandId = match?.groups?.commandId; + let kbLabel; if (match?.length && commandId) { const keybinding = keybindingService.lookupKeybinding(commandId)?.getAriaLabel(); - let kbLabel = keybinding; - if (!kbLabel) { - const args = URI.parse(`command:workbench.action.openGlobalKeybindings?${encodeURIComponent(JSON.stringify(commandId))}`); - kbLabel = ` [Configure a keybinding](${args})`; + if (!keybinding) { + const configureKb = keybindingService.lookupKeybinding(AccessibilityCommandId.AccessibilityHelpConfigureKeybindings)?.getAriaLabel(); + const keybindingToConfigureQuickPick = configureKb ? '(' + configureKb + ')' : 'by assigning a keybinding to the command accessibility.openQuickPick.'; + kbLabel = `, configure a keybinding ` + keybindingToConfigureQuickPick; + configureKeybindingItems.push({ + label: commandId, + id: commandId + }); } else { kbLabel = ' (' + keybinding + ')'; } resolvedContent = resolvedContent.replace(match[0], kbLabel); } } - const result = new MarkdownString(resolvedContent); - result.isTrusted = true; - return result; + const value = new MarkdownString(resolvedContent); + value.isTrusted = true; + return { value, configureKeybindingItems: configureKeybindingItems.length ? configureKeybindingItems : undefined }; } + diff --git a/src/vs/workbench/contrib/accessibility/common/accessibilityCommands.ts b/src/vs/workbench/contrib/accessibility/common/accessibilityCommands.ts index 53ad46e7846..4c5e852c355 100644 --- a/src/vs/workbench/contrib/accessibility/common/accessibilityCommands.ts +++ b/src/vs/workbench/contrib/accessibility/common/accessibilityCommands.ts @@ -12,5 +12,6 @@ export const enum AccessibilityCommandId { ShowPrevious = 'editor.action.accessibleViewPrevious', AccessibleViewAcceptInlineCompletion = 'editor.action.accessibleViewAcceptInlineCompletion', NextCodeBlock = 'editor.action.accessibleViewNextCodeBlock', - PreviousCodeBlock = 'editor.action.accessibleViewPreviousCodeBlock' + PreviousCodeBlock = 'editor.action.accessibleViewPreviousCodeBlock', + AccessibilityHelpConfigureKeybindings = 'editor.action.accessibilityHelpConfigureKeybindings', } From 6d7771d8a2f39ed837496deef125d15e45eb3ba1 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Sun, 19 May 2024 08:04:46 +0100 Subject: [PATCH 266/357] Register child instantiation service (#213010) ref #212879 --- src/vs/workbench/contrib/chat/browser/chatQuick.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatQuick.ts b/src/vs/workbench/contrib/chat/browser/chatQuick.ts index adc1a6eb791..f40ad589c60 100644 --- a/src/vs/workbench/contrib/chat/browser/chatQuick.ts +++ b/src/vs/workbench/contrib/chat/browser/chatQuick.ts @@ -206,14 +206,15 @@ class QuickChat extends Disposable { render(parent: HTMLElement): void { if (this.widget) { + // NOTE: if this changes, we need to make sure disposables in this function are tracked differently. throw new Error('Cannot render quick chat twice'); } - const scopedInstantiationService = this.instantiationService.createChild( + const scopedInstantiationService = this._register(this.instantiationService.createChild( new ServiceCollection([ IContextKeyService, this._register(this.contextKeyService.createScoped(parent)) ]) - ); + )); this.widget = this._register( scopedInstantiationService.createInstance( ChatWidget, From b9a90604f978cad3f725cf53d5058dd65348b45c Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Sun, 19 May 2024 00:05:23 -0700 Subject: [PATCH 267/357] Limit image width in chat (#213021) --- src/vs/workbench/contrib/chat/browser/media/chat.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 97c1103e53d..f075098745a 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -271,6 +271,10 @@ line-height: 1.3rem; } +.interactive-item-container .value .rendered-markdown img { + max-width: 100%; +} + .interactive-item-container .monaco-tokenized-source, .interactive-item-container code { font-family: var(--monaco-monospace-font); From 7c8252c59b8025c05bdeb10eb62d1b7e1c3fa9dd Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sun, 19 May 2024 10:42:14 +0200 Subject: [PATCH 268/357] Make sure child instantiation service instances are disposed/tracked for disposal (#212879) (#213002) --- src/vs/workbench/browser/parts/compositePart.ts | 4 ++-- src/vs/workbench/browser/parts/editor/auxiliaryEditorPart.ts | 4 ++-- src/vs/workbench/browser/parts/editor/editorGroupView.ts | 4 ++-- src/vs/workbench/browser/parts/editor/editorPart.ts | 4 ++-- src/vs/workbench/browser/parts/editor/editorStatus.ts | 4 ++-- src/vs/workbench/browser/parts/editor/editorTabsControl.ts | 4 ++-- .../languageStatus/browser/languageStatus.contribution.ts | 4 ++-- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/vs/workbench/browser/parts/compositePart.ts b/src/vs/workbench/browser/parts/compositePart.ts index 19c56d9b509..c86d7fd78f5 100644 --- a/src/vs/workbench/browser/parts/compositePart.ts +++ b/src/vs/workbench/browser/parts/compositePart.ts @@ -187,9 +187,9 @@ export abstract class CompositePart extends Part { this._register(that.onDidCompositeClose.event(e => this.onScopeClosed(e.getId()))); } }()); - const compositeInstantiationService = this.instantiationService.createChild(new ServiceCollection( + const compositeInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection( [IEditorProgressService, compositeProgressIndicator] // provide the editor progress service for any editors instantiated within the composite - )); + ))); const composite = compositeDescriptor.instantiate(compositeInstantiationService); const disposable = new DisposableStore(); diff --git a/src/vs/workbench/browser/parts/editor/auxiliaryEditorPart.ts b/src/vs/workbench/browser/parts/editor/auxiliaryEditorPart.ts index f0341568cf0..279fc5713c4 100644 --- a/src/vs/workbench/browser/parts/editor/auxiliaryEditorPart.ts +++ b/src/vs/workbench/browser/parts/editor/auxiliaryEditorPart.ts @@ -203,10 +203,10 @@ export class AuxiliaryEditorPart { auxiliaryWindow.layout(); // Have a InstantiationService that is scoped to the auxiliary window - const instantiationService = this.instantiationService.createChild(new ServiceCollection( + const instantiationService = disposables.add(this.instantiationService.createChild(new ServiceCollection( [IStatusbarService, this.statusbarService.createScoped(statusbarPart, disposables)], [IEditorService, this.editorService.createScoped(editorPart, disposables)] - )); + ))); return { part: editorPart, diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 4dc40e9aba9..d847ef50137 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -197,10 +197,10 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.progressBar.hide(); // Scoped instantiation service - this.scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection( + this.scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection( [IContextKeyService, this.scopedContextKeyService], [IEditorProgressService, this._register(new EditorProgressIndicator(this.progressBar, this))] - )); + ))); // Context keys this.resourceContext = this._register(this.scopedInstantiationService.createInstance(ResourceContextKey)); diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index 1fa0dcbeb39..e467ab0270e 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -978,9 +978,9 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView { // Scoped instantiation service const scopedContextKeyService = this._register(this.contextKeyService.createScoped(this.container)); - this.scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection( + this.scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection( [IContextKeyService, scopedContextKeyService] - )); + ))); // Grid control this._willRestoreState = !options || options.restorePreviousState; diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index dc58c035fed..c2ceeb2d7d7 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -893,9 +893,9 @@ export class EditorStatusContribution extends Disposable implements IWorkbenchCo super(); // Main Editor Status - const mainInstantiationService = instantiationService.createChild(new ServiceCollection( + const mainInstantiationService = this._register(instantiationService.createChild(new ServiceCollection( [IEditorService, editorService.createScoped('main', this._store)] - )); + ))); this._register(mainInstantiationService.createInstance(EditorStatus, mainWindow.vscodeWindowId)); // Auxiliary Editor Status diff --git a/src/vs/workbench/browser/parts/editor/editorTabsControl.ts b/src/vs/workbench/browser/parts/editor/editorTabsControl.ts index 0031e18c565..e006ebd9040 100644 --- a/src/vs/workbench/browser/parts/editor/editorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/editorTabsControl.ts @@ -146,9 +146,9 @@ export abstract class EditorTabsControl extends Themable implements IEditorTabsC super(themeService); this.contextMenuContextKeyService = this._register(this.contextKeyService.createScoped(parent)); - const scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection( + const scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection( [IContextKeyService, this.contextMenuContextKeyService], - )); + ))); this.resourceContext = this._register(scopedInstantiationService.createInstance(ResourceContextKey)); diff --git a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts index f9fa228eb39..fd3f51d3560 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts +++ b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts @@ -72,9 +72,9 @@ class LanguageStatusContribution extends Disposable implements IWorkbenchContrib super(); // --- main language status - const mainInstantiationService = instantiationService.createChild(new ServiceCollection( + const mainInstantiationService = this._register(instantiationService.createChild(new ServiceCollection( [IEditorService, editorService.createScoped('main', this._store)] - )); + ))); this._register(mainInstantiationService.createInstance(LanguageStatus)); // --- auxiliary language status From 5a514567983683ec5561c1e5ea45c286601164d6 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 20 May 2024 09:50:50 +0200 Subject: [PATCH 269/357] voice - limit `Escape` key to context key scopes (might help for #213017) (#213049) --- .../actions/voiceChatActions.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts index c2a0603c397..86454907209 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts @@ -188,15 +188,15 @@ class VoiceChatSessionControllerFactory { switch (state) { case VoiceChatSessionState.GettingReady: contextVoiceChatGettingReady.set(true); - contextVoiceChatInProgress.set(undefined); + contextVoiceChatInProgress.reset(); break; case VoiceChatSessionState.Started: - contextVoiceChatGettingReady.set(false); + contextVoiceChatGettingReady.reset(); contextVoiceChatInProgress.set(context); break; case VoiceChatSessionState.Stopped: - contextVoiceChatGettingReady.set(false); - contextVoiceChatInProgress.set(undefined); + contextVoiceChatGettingReady.reset(); + contextVoiceChatInProgress.reset(); break; } }; @@ -629,7 +629,8 @@ export class StopListeningAction extends Action2 { f1: true, keybinding: { weight: KeybindingWeight.WorkbenchContrib + 100, - primary: KeyCode.Escape + primary: KeyCode.Escape, + when: AnyScopedVoiceChatInProgress }, icon: spinningLoading, precondition: GlobalVoiceChatInProgress, // need global context here because of `f1: true` @@ -775,7 +776,7 @@ class ChatSynthesizerSessions { disposables.add(controller.onDidHideChat(() => this.stop())); const scopedChatToSpeechInProgress = ScopedChatSynthesisInProgress.bindTo(controller.contextKeyService); - disposables.add(toDisposable(() => scopedChatToSpeechInProgress.set(false))); + disposables.add(toDisposable(() => scopedChatToSpeechInProgress.reset())); disposables.add(session.onDidChange(e => { switch (e.status) { @@ -783,7 +784,7 @@ class ChatSynthesizerSessions { scopedChatToSpeechInProgress.set(true); break; case TextToSpeechStatus.Stopped: - scopedChatToSpeechInProgress.set(false); + scopedChatToSpeechInProgress.reset(); break; } })); @@ -917,6 +918,7 @@ export class StopReadAloud extends Action2 { keybinding: { weight: KeybindingWeight.WorkbenchContrib + 100, primary: KeyCode.Escape, + when: ScopedChatSynthesisInProgress }, menu: [ { @@ -1237,7 +1239,7 @@ abstract class BaseInstallSpeechProviderAction extends Action2 { enable: true }, ProgressLocation.Notification); } finally { - InstallingSpeechProvider.bindTo(contextKeyService).set(false); + InstallingSpeechProvider.bindTo(contextKeyService).reset(); } } From aa31bfc9fd1746626b3efe86f41b9c172d5f4d23 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Mon, 20 May 2024 13:03:38 +0200 Subject: [PATCH 270/357] Cleanup editor group context keys (#212955) * cleanup editor group context keys * Update src/vs/workbench/browser/parts/editor/editorPart.ts Co-authored-by: Benjamin Pasero * context key on parts * Update global context keys * remove scoped keys on group removal * cleanup --------- Co-authored-by: Benjamin Pasero --- src/vs/workbench/browser/contextkeys.ts | 119 +----------------- .../workbench/browser/parts/editor/editor.ts | 3 + .../browser/parts/editor/editorGroupView.ts | 39 +++--- .../browser/parts/editor/editorParts.ts | 81 +++++++++++- 4 files changed, 102 insertions(+), 140 deletions(-) diff --git a/src/vs/workbench/browser/contextkeys.ts b/src/vs/workbench/browser/contextkeys.ts index 71ec7e27232..fb8919b8e9a 100644 --- a/src/vs/workbench/browser/contextkeys.ts +++ b/src/vs/workbench/browser/contextkeys.ts @@ -7,56 +7,29 @@ import { Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { IContextKeyService, IContextKey, setConstant as setConstantContextKey } from 'vs/platform/contextkey/common/contextkey'; import { InputFocusedContext, IsMacContext, IsLinuxContext, IsWindowsContext, IsWebContext, IsMacNativeContext, IsDevelopmentContext, IsIOSContext, ProductQualityContext, IsMobileContext } from 'vs/platform/contextkey/common/contextkeys'; -import { SplitEditorsVertically, InEditorZenModeContext, ActiveEditorCanRevertContext, ActiveEditorGroupLockedContext, ActiveEditorCanSplitInGroupContext, SideBySideEditorActiveContext, AuxiliaryBarVisibleContext, SideBarVisibleContext, PanelAlignmentContext, PanelMaximizedContext, PanelVisibleContext, ActiveEditorContext, EditorsVisibleContext, TextCompareEditorVisibleContext, TextCompareEditorActiveContext, ActiveEditorGroupEmptyContext, EmbedderIdentifierContext, EditorTabsVisibleContext, IsMainEditorCenteredLayoutContext, ActiveEditorGroupIndexContext, ActiveEditorGroupLastContext, ActiveEditorReadonlyContext, MainEditorAreaVisibleContext, ActiveEditorAvailableEditorIdsContext, DirtyWorkingCopiesContext, EmptyWorkspaceSupportContext, EnterMultiRootWorkspaceSupportContext, HasWebFileSystemAccess, IsMainWindowFullscreenContext, OpenFolderWorkspaceSupportContext, RemoteNameContext, VirtualWorkspaceContext, WorkbenchStateContext, WorkspaceFolderCountContext, PanelPositionContext, TemporaryWorkspaceContext, ActiveEditorCanToggleReadonlyContext, applyAvailableEditorIds, TitleBarVisibleContext, TitleBarStyleContext, MultipleEditorGroupsContext, IsAuxiliaryWindowFocusedContext, ActiveCompareEditorCanSwapContext } from 'vs/workbench/common/contextkeys'; -import { TEXT_DIFF_EDITOR_ID, EditorInputCapabilities, SIDE_BY_SIDE_EDITOR_ID, EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; +import { SplitEditorsVertically, InEditorZenModeContext, AuxiliaryBarVisibleContext, SideBarVisibleContext, PanelAlignmentContext, PanelMaximizedContext, PanelVisibleContext, EmbedderIdentifierContext, EditorTabsVisibleContext, IsMainEditorCenteredLayoutContext, MainEditorAreaVisibleContext, DirtyWorkingCopiesContext, EmptyWorkspaceSupportContext, EnterMultiRootWorkspaceSupportContext, HasWebFileSystemAccess, IsMainWindowFullscreenContext, OpenFolderWorkspaceSupportContext, RemoteNameContext, VirtualWorkspaceContext, WorkbenchStateContext, WorkspaceFolderCountContext, PanelPositionContext, TemporaryWorkspaceContext, TitleBarVisibleContext, TitleBarStyleContext, IsAuxiliaryWindowFocusedContext } from 'vs/workbench/common/contextkeys'; import { trackFocus, addDisposableListener, EventType, onDidRegisterWindow, getActiveWindow } from 'vs/base/browser/dom'; import { preferredSideBySideGroupDirection, GroupDirection, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { WorkbenchState, IWorkspaceContextService, isTemporaryWorkspace } from 'vs/platform/workspace/common/workspace'; import { IWorkbenchLayoutService, Parts, positionToString } from 'vs/workbench/services/layout/browser/layoutService'; import { getRemoteName } from 'vs/platform/remote/common/remoteHosts'; import { getVirtualWorkspaceScheme } from 'vs/platform/workspace/common/virtualWorkspace'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { isNative } from 'vs/base/common/platform'; -import { IEditorResolverService } from 'vs/workbench/services/editor/common/editorResolverService'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; import { WebFileSystemAccess } from 'vs/platform/files/browser/webFileSystemAccess'; import { IProductService } from 'vs/platform/product/common/productService'; -import { FileSystemProviderCapabilities, IFileService } from 'vs/platform/files/common/files'; import { getTitleBarStyle } from 'vs/platform/window/common/window'; import { mainWindow } from 'vs/base/browser/window'; -import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { isFullscreen, onDidChangeFullscreen } from 'vs/base/browser/browser'; -import { Schemas } from 'vs/base/common/network'; export class WorkbenchContextKeysHandler extends Disposable { private inputFocusedContext: IContextKey; private dirtyWorkingCopiesContext: IContextKey; - private activeEditorContext: IContextKey; - private activeEditorCanRevert: IContextKey; - private activeEditorCanSplitInGroup: IContextKey; - private activeEditorAvailableEditorIds: IContextKey; - - private activeEditorIsReadonly: IContextKey; - private activeCompareEditorCanSwap: IContextKey; - private activeEditorCanToggleReadonly: IContextKey; - - private activeEditorGroupEmpty: IContextKey; - private activeEditorGroupIndex: IContextKey; - private activeEditorGroupLast: IContextKey; - private activeEditorGroupLocked: IContextKey; - private multipleEditorGroupsContext: IContextKey; - - private editorsVisibleContext: IContextKey; - - private textCompareEditorVisibleContext: IContextKey; - private textCompareEditorActiveContext: IContextKey; - - private sideBySideEditorActiveContext: IContextKey; private splitEditorsVerticallyContext: IContextKey; private workbenchStateContext: IContextKey; @@ -90,13 +63,10 @@ export class WorkbenchContextKeysHandler extends Disposable { @IConfigurationService private readonly configurationService: IConfigurationService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IProductService private readonly productService: IProductService, - @IEditorService private readonly editorService: IEditorService, - @IEditorResolverService private readonly editorResolverService: IEditorResolverService, @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService, @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, - @IFileService private readonly fileService: IFileService ) { super(); @@ -128,24 +98,6 @@ export class WorkbenchContextKeysHandler extends Disposable { ProductQualityContext.bindTo(this.contextKeyService).set(this.productService.quality || ''); EmbedderIdentifierContext.bindTo(this.contextKeyService).set(productService.embedderIdentifier); - // Editors - this.activeEditorContext = ActiveEditorContext.bindTo(this.contextKeyService); - this.activeEditorIsReadonly = ActiveEditorReadonlyContext.bindTo(this.contextKeyService); - this.activeCompareEditorCanSwap = ActiveCompareEditorCanSwapContext.bindTo(this.contextKeyService); - this.activeEditorCanToggleReadonly = ActiveEditorCanToggleReadonlyContext.bindTo(this.contextKeyService); - this.activeEditorCanRevert = ActiveEditorCanRevertContext.bindTo(this.contextKeyService); - this.activeEditorCanSplitInGroup = ActiveEditorCanSplitInGroupContext.bindTo(this.contextKeyService); - this.activeEditorAvailableEditorIds = ActiveEditorAvailableEditorIdsContext.bindTo(this.contextKeyService); - this.editorsVisibleContext = EditorsVisibleContext.bindTo(this.contextKeyService); - this.textCompareEditorVisibleContext = TextCompareEditorVisibleContext.bindTo(this.contextKeyService); - this.textCompareEditorActiveContext = TextCompareEditorActiveContext.bindTo(this.contextKeyService); - this.sideBySideEditorActiveContext = SideBySideEditorActiveContext.bindTo(this.contextKeyService); - this.activeEditorGroupEmpty = ActiveEditorGroupEmptyContext.bindTo(this.contextKeyService); - this.activeEditorGroupIndex = ActiveEditorGroupIndexContext.bindTo(this.contextKeyService); - this.activeEditorGroupLast = ActiveEditorGroupLastContext.bindTo(this.contextKeyService); - this.activeEditorGroupLocked = ActiveEditorGroupLockedContext.bindTo(this.contextKeyService); - this.multipleEditorGroupsContext = MultipleEditorGroupsContext.bindTo(this.contextKeyService); - // Working Copies this.dirtyWorkingCopiesContext = DirtyWorkingCopiesContext.bindTo(this.contextKeyService); this.dirtyWorkingCopiesContext.set(this.workingCopyService.hasDirty); @@ -231,19 +183,8 @@ export class WorkbenchContextKeysHandler extends Disposable { private registerListeners(): void { this.editorGroupService.whenReady.then(() => { this.updateEditorAreaContextKeys(); - this.updateEditorContextKeys(); }); - this._register(this.editorService.onDidActiveEditorChange(() => this.updateEditorContextKeys())); - this._register(this.editorService.onDidVisibleEditorsChange(() => this.updateEditorContextKeys())); - - this._register(this.editorGroupService.onDidAddGroup(() => this.updateEditorContextKeys())); - this._register(this.editorGroupService.onDidRemoveGroup(() => this.updateEditorContextKeys())); - this._register(this.editorGroupService.onDidChangeGroupIndex(() => this.updateEditorContextKeys())); - - this._register(this.editorGroupService.onDidChangeActiveGroup(() => this.updateEditorGroupContextKeys())); - this._register(this.editorGroupService.onDidChangeGroupLocked(() => this.updateEditorGroupContextKeys())); - this._register(this.editorGroupService.onDidChangeEditorPartOptions(() => this.updateEditorAreaContextKeys())); this._register(Event.runAndSubscribe(onDidRegisterWindow, ({ window, disposables }) => disposables.add(addDisposableListener(window, EventType.FOCUS_IN, () => this.updateInputContextKeys(window.document), true)), { window: mainWindow, disposables: this._store })); @@ -290,64 +231,6 @@ export class WorkbenchContextKeysHandler extends Disposable { this.editorTabsVisibleContext.set(this.editorGroupService.partOptions.showTabs === 'multiple'); } - private updateEditorContextKeys(): void { - const activeEditorPane = this.editorService.activeEditorPane; - const visibleEditorPanes = this.editorService.visibleEditorPanes; - - this.textCompareEditorActiveContext.set(activeEditorPane?.getId() === TEXT_DIFF_EDITOR_ID); - this.textCompareEditorVisibleContext.set(visibleEditorPanes.some(editorPane => editorPane.getId() === TEXT_DIFF_EDITOR_ID)); - - this.sideBySideEditorActiveContext.set(activeEditorPane?.getId() === SIDE_BY_SIDE_EDITOR_ID); - - if (visibleEditorPanes.length > 0) { - this.editorsVisibleContext.set(true); - } else { - this.editorsVisibleContext.reset(); - } - - if (!this.editorService.activeEditor) { - this.activeEditorGroupEmpty.set(true); - } else { - this.activeEditorGroupEmpty.reset(); - } - - this.updateEditorGroupContextKeys(); - - if (activeEditorPane) { - this.activeEditorContext.set(activeEditorPane.getId()); - this.activeEditorCanRevert.set(!activeEditorPane.input.hasCapability(EditorInputCapabilities.Untitled)); - this.activeEditorCanSplitInGroup.set(activeEditorPane.input.hasCapability(EditorInputCapabilities.CanSplitInGroup)); - applyAvailableEditorIds(this.activeEditorAvailableEditorIds, activeEditorPane.input, this.editorResolverService); - this.activeEditorIsReadonly.set(!!activeEditorPane.input.isReadonly()); - const primaryEditorResource = EditorResourceAccessor.getOriginalUri(activeEditorPane.input, { supportSideBySide: SideBySideEditor.PRIMARY }); - const secondaryEditorResource = EditorResourceAccessor.getOriginalUri(activeEditorPane.input, { supportSideBySide: SideBySideEditor.SECONDARY }); - this.activeCompareEditorCanSwap.set(activeEditorPane.input instanceof DiffEditorInput && !activeEditorPane.input.original.isReadonly() && !!primaryEditorResource && (this.fileService.hasProvider(primaryEditorResource) || primaryEditorResource.scheme === Schemas.untitled) && !!secondaryEditorResource && (this.fileService.hasProvider(secondaryEditorResource) || secondaryEditorResource.scheme === Schemas.untitled)); - this.activeEditorCanToggleReadonly.set(!!primaryEditorResource && this.fileService.hasProvider(primaryEditorResource) && !this.fileService.hasCapability(primaryEditorResource, FileSystemProviderCapabilities.Readonly)); - } else { - this.activeEditorContext.reset(); - this.activeEditorIsReadonly.reset(); - this.activeCompareEditorCanSwap.reset(); - this.activeEditorCanToggleReadonly.reset(); - this.activeEditorCanRevert.reset(); - this.activeEditorCanSplitInGroup.reset(); - this.activeEditorAvailableEditorIds.reset(); - } - } - - private updateEditorGroupContextKeys(): void { - const groupCount = this.editorGroupService.count; - if (groupCount > 1) { - this.multipleEditorGroupsContext.set(true); - } else { - this.multipleEditorGroupsContext.reset(); - } - - const activeGroup = this.editorGroupService.activeGroup; - this.activeEditorGroupIndex.set(activeGroup.index + 1); // not zero-indexed - this.activeEditorGroupLast.set(activeGroup.index === groupCount - 1); - this.activeEditorGroupLocked.set(activeGroup.isLocked); - } - private updateInputContextKeys(ownerDocument: Document): void { function activeElementIsInput(): boolean { diff --git a/src/vs/workbench/browser/parts/editor/editor.ts b/src/vs/workbench/browser/parts/editor/editor.ts index 6b9ad992bd4..1bf1de43220 100644 --- a/src/vs/workbench/browser/parts/editor/editor.ts +++ b/src/vs/workbench/browser/parts/editor/editor.ts @@ -18,6 +18,7 @@ import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { IWindowsConfiguration } from 'vs/platform/window/common/window'; import { BooleanVerifier, EnumVerifier, NumberVerifier, ObjectVerifier, SetVerifier, verifyObject } from 'vs/base/common/verifier'; import { IAuxiliaryWindowOpenOptions } from 'vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService'; +import { ContextKeyValue, IContextKey, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; export interface IEditorPartCreationOptions { readonly restorePreviousState: boolean; @@ -187,6 +188,8 @@ export interface IEditorPartsView { readonly count: number; createAuxiliaryEditorPart(options?: IAuxiliaryWindowOpenOptions): Promise; + + bind(contextKey: RawContextKey, group: IEditorGroupView): IContextKey; } /** diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index d847ef50137..330c90fd1ae 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -248,27 +248,28 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } private handleGroupContextKeys(): void { - const groupActiveEditorDirtyContext = ActiveEditorDirtyContext.bindTo(this.scopedContextKeyService); - const groupActiveEditorPinnedContext = ActiveEditorPinnedContext.bindTo(this.scopedContextKeyService); - const groupActiveEditorFirstContext = ActiveEditorFirstInGroupContext.bindTo(this.scopedContextKeyService); - const groupActiveEditorLastContext = ActiveEditorLastInGroupContext.bindTo(this.scopedContextKeyService); - const groupActiveEditorStickyContext = ActiveEditorStickyContext.bindTo(this.scopedContextKeyService); - const groupEditorsCountContext = EditorGroupEditorsCountContext.bindTo(this.scopedContextKeyService); - const groupLockedContext = ActiveEditorGroupLockedContext.bindTo(this.scopedContextKeyService); - const multipleEditorsSelectedContext = MultipleEditorsSelectedContext.bindTo(this.contextKeyService); - const twoEditorsSelectedContext = TwoEditorsSelectedContext.bindTo(this.contextKeyService); + const groupActiveEditorDirtyContext = this.editorPartsView.bind(ActiveEditorDirtyContext, this); + const groupActiveEditorPinnedContext = this.editorPartsView.bind(ActiveEditorPinnedContext, this); + const groupActiveEditorFirstContext = this.editorPartsView.bind(ActiveEditorFirstInGroupContext, this); + const groupActiveEditorLastContext = this.editorPartsView.bind(ActiveEditorLastInGroupContext, this); + const groupActiveEditorStickyContext = this.editorPartsView.bind(ActiveEditorStickyContext, this); + const groupEditorsCountContext = this.editorPartsView.bind(EditorGroupEditorsCountContext, this); + const groupLockedContext = this.editorPartsView.bind(ActiveEditorGroupLockedContext, this); - const groupActiveEditorContext = ActiveEditorContext.bindTo(this.scopedContextKeyService); - const groupActiveEditorIsReadonly = ActiveEditorReadonlyContext.bindTo(this.scopedContextKeyService); - const groupActiveEditorCanRevert = ActiveEditorCanRevertContext.bindTo(this.scopedContextKeyService); - const groupActiveEditorCanToggleReadonly = ActiveEditorCanToggleReadonlyContext.bindTo(this.scopedContextKeyService); - const groupActiveCompareEditorCanSwap = ActiveCompareEditorCanSwapContext.bindTo(this.scopedContextKeyService); - const groupTextCompareEditorVisibleContext = TextCompareEditorVisibleContext.bindTo(this.scopedContextKeyService); - const groupTextCompareEditorActiveContext = TextCompareEditorActiveContext.bindTo(this.scopedContextKeyService); + const multipleEditorsSelectedContext = MultipleEditorsSelectedContext.bindTo(this.scopedContextKeyService); + const twoEditorsSelectedContext = TwoEditorsSelectedContext.bindTo(this.scopedContextKeyService); - const groupActiveEditorAvailableEditorIds = ActiveEditorAvailableEditorIdsContext.bindTo(this.scopedContextKeyService); - const groupActiveEditorCanSplitInGroupContext = ActiveEditorCanSplitInGroupContext.bindTo(this.scopedContextKeyService); - const sideBySideEditorContext = SideBySideEditorActiveContext.bindTo(this.scopedContextKeyService); + const groupActiveEditorContext = this.editorPartsView.bind(ActiveEditorContext, this); + const groupActiveEditorIsReadonly = this.editorPartsView.bind(ActiveEditorReadonlyContext, this); + const groupActiveEditorCanRevert = this.editorPartsView.bind(ActiveEditorCanRevertContext, this); + const groupActiveEditorCanToggleReadonly = this.editorPartsView.bind(ActiveEditorCanToggleReadonlyContext, this); + const groupActiveCompareEditorCanSwap = this.editorPartsView.bind(ActiveCompareEditorCanSwapContext, this); + const groupTextCompareEditorVisibleContext = this.editorPartsView.bind(TextCompareEditorVisibleContext, this); + const groupTextCompareEditorActiveContext = this.editorPartsView.bind(TextCompareEditorActiveContext, this); + + const groupActiveEditorAvailableEditorIds = this.editorPartsView.bind(ActiveEditorAvailableEditorIdsContext, this); + const groupActiveEditorCanSplitInGroupContext = this.editorPartsView.bind(ActiveEditorCanSplitInGroupContext, this); + const sideBySideEditorContext = this.editorPartsView.bind(SideBySideEditorActiveContext, this); const activeEditorListener = this._register(new MutableDisposable()); diff --git a/src/vs/workbench/browser/parts/editor/editorParts.ts b/src/vs/workbench/browser/parts/editor/editorParts.ts index 9a1d52cb34a..6b7a1907005 100644 --- a/src/vs/workbench/browser/parts/editor/editorParts.ts +++ b/src/vs/workbench/browser/parts/editor/editorParts.ts @@ -20,6 +20,7 @@ import { IStorageService, IStorageValueChangeEvent, StorageScope, StorageTarget import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IAuxiliaryWindowOpenOptions, IAuxiliaryWindowService } from 'vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService'; import { generateUuid } from 'vs/base/common/uuid'; +import { ContextKeyValue, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; interface IEditorPartsUIState { readonly auxiliary: IAuxiliaryEditorPartState[]; @@ -48,7 +49,8 @@ export class EditorParts extends MultiWindowParts implements IEditor @IInstantiationService private readonly instantiationService: IInstantiationService, @IStorageService private readonly storageService: IStorageService, @IThemeService themeService: IThemeService, - @IAuxiliaryWindowService private readonly auxiliaryWindowService: IAuxiliaryWindowService + @IAuxiliaryWindowService private readonly auxiliaryWindowService: IAuxiliaryWindowService, + @IContextKeyService private readonly contextKeyService: IContextKeyService ) { super('workbench.editorParts', themeService, storageService); @@ -121,9 +123,15 @@ export class EditorParts extends MultiWindowParts implements IEditor })); disposables.add(toDisposable(() => this.doUpdateMostRecentActive(part))); - disposables.add(part.onDidChangeActiveGroup(group => this._onDidActiveGroupChange.fire(group))); + disposables.add(part.onDidChangeActiveGroup(group => { + this.updateGlobalContextKeys(); + this._onDidActiveGroupChange.fire(group); + })); disposables.add(part.onDidAddGroup(group => this._onDidAddGroup.fire(group))); - disposables.add(part.onDidRemoveGroup(group => this._onDidRemoveGroup.fire(group))); + disposables.add(part.onDidRemoveGroup(group => { + this.removeGroupScopedContextKeys(group); + this._onDidRemoveGroup.fire(group); + })); disposables.add(part.onDidMoveGroup(group => this._onDidMoveGroup.fire(group))); disposables.add(part.onDidActivateGroup(group => this._onDidActivateGroup.fire(group))); disposables.add(part.onDidChangeGroupMaximized(maximized => this._onDidChangeGroupMaximized.fire(maximized))); @@ -627,6 +635,73 @@ export class EditorParts extends MultiWindowParts implements IEditor return this.getPart(container).createEditorDropTarget(container, delegate); } + private readonly globalContextKeys = new Map>(); + private readonly scopedContextKeys = new Map>>(); + + bind(contextKey: RawContextKey, group: IEditorGroupView): IContextKey { + + // Ensure we only bind to the same context key once globaly + let globalContextKey = this.globalContextKeys.get(contextKey.key); + if (!globalContextKey) { + globalContextKey = contextKey.bindTo(this.contextKeyService); + this.globalContextKeys.set(contextKey.key, globalContextKey); + } + + // Ensure we only bind to the same context key once per group + let groupScopedContextKeys = this.scopedContextKeys.get(group.id); + if (!groupScopedContextKeys) { + groupScopedContextKeys = new Map>(); + this.scopedContextKeys.set(group.id, groupScopedContextKeys); + } + let scopedContextKey = groupScopedContextKeys.get(contextKey.key); + if (!scopedContextKey) { + scopedContextKey = contextKey.bindTo(group.scopedContextKeyService); + groupScopedContextKeys.set(contextKey.key, scopedContextKey); + } + + const that = this; + return { + get(): T | undefined { + return scopedContextKey.get() as T | undefined; + }, + set(value: T): void { + if (that.activeGroup === group) { + globalContextKey.set(value); + } + scopedContextKey.set(value); + }, + reset(): void { + if (that.activeGroup === group) { + globalContextKey.reset(); + } + scopedContextKey.reset(); + }, + }; + } + + private updateGlobalContextKeys(): void { + const activeGroupScopedContextKeys = this.scopedContextKeys.get(this.activeGroup.id); + if (!activeGroupScopedContextKeys) { + return; + } + + for (const [key, globalContextKey] of this.globalContextKeys) { + const scopedContextKey = activeGroupScopedContextKeys.get(key); + if (scopedContextKey) { + globalContextKey.set(scopedContextKey.get()); + } else { + globalContextKey.reset(); + } + } + } + + private removeGroupScopedContextKeys(group: IEditorGroupView): void { + const groupScopedContextKeys = this.scopedContextKeys.get(group.id); + if (groupScopedContextKeys) { + this.scopedContextKeys.delete(group.id); + } + } + //#endregion //#region Main Editor Part Only From 28ebd9176fd87f93c00d4ce5dbec24b9ab72908d Mon Sep 17 00:00:00 2001 From: Robo Date: Tue, 21 May 2024 00:31:44 +0900 Subject: [PATCH 271/357] chore: bump electron@29.4.0 (#213050) * chore: bump electron@29.4.0 * chore: remove io_uring workaround * chore: bump distro * chore: update dialog result for canceled save dialogs Refs https://github.com/electron/electron/commit/fe01ed750ad982e2d67e5be631171399e8428841 * chore: add back io_uring workaround for remote oss tests * chore: update nodejs v20.9.0 build * chore: add back io_uring workaround for remote tests --- .yarnrc | 4 +- .../linux/product-build-linux-test.yml | 32 +--- build/checksums/electron.txt | 150 +++++++++--------- cgmanifest.json | 4 +- package.json | 4 +- remote/.yarnrc | 2 +- resources/server/bin/code-server-linux.sh | 4 +- .../electron-main/dialogMainService.ts | 2 +- yarn.lock | 8 +- 9 files changed, 96 insertions(+), 114 deletions(-) diff --git a/.yarnrc b/.yarnrc index 31ceae81a48..b40fb7e7f58 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1,5 +1,5 @@ disturl "https://electronjs.org/headers" -target "29.3.1" -ms_build_id "9464424" +target "29.4.0" +ms_build_id "9593362" runtime "electron" build_from_source "true" diff --git a/build/azure-pipelines/linux/product-build-linux-test.yml b/build/azure-pipelines/linux/product-build-linux-test.yml index 4e225757a81..4968b9ff04f 100644 --- a/build/azure-pipelines/linux/product-build-linux-test.yml +++ b/build/azure-pipelines/linux/product-build-linux-test.yml @@ -92,9 +92,6 @@ steps: - script: ./scripts/test-integration.sh --tfs "Integration Tests" env: DISPLAY: ":10" - # TODO(deepak1556): Remove this once runtime is updated for - # https://github.com/microsoft/vscode/issues/210467#issuecomment-2104566724 - UV_USE_IO_URING: 0 displayName: Run integration tests (Electron) timeoutInMinutes: 20 @@ -104,8 +101,7 @@ steps: - script: ./scripts/test-remote-integration.sh env: - # TODO(deepak1556): Remove this once runtime is updated for - # https://github.com/microsoft/vscode/issues/210467#issuecomment-2104566724 + # TODO(deepak1556): Remove this once we update to Node.js >= 20.11.x UV_USE_IO_URING: 0 displayName: Run integration tests (Remote) timeoutInMinutes: 20 @@ -123,9 +119,6 @@ steps: ./scripts/test-integration.sh --build --tfs "Integration Tests" env: VSCODE_REMOTE_SERVER_PATH: $(agent.builddirectory)/vscode-server-linux-$(VSCODE_ARCH) - # TODO(deepak1556): Remove this once runtime is updated for - # https://github.com/microsoft/vscode/issues/210467#issuecomment-2104566724 - UV_USE_IO_URING: 0 displayName: Run integration tests (Electron) timeoutInMinutes: 20 @@ -144,8 +137,7 @@ steps: ./scripts/test-remote-integration.sh env: VSCODE_REMOTE_SERVER_PATH: $(agent.builddirectory)/vscode-server-linux-$(VSCODE_ARCH) - # TODO(deepak1556): Remove this once runtime is updated for - # https://github.com/microsoft/vscode/issues/210467#issuecomment-2104566724 + # TODO(deepak1556): Remove this once we update to Node.js >= 20.11.x UV_USE_IO_URING: 0 displayName: Run integration tests (Remote) timeoutInMinutes: 20 @@ -173,42 +165,31 @@ steps: - script: yarn smoketest-no-compile --tracing timeoutInMinutes: 20 - env: - # TODO(deepak1556): Remove this once runtime is updated for - # https://github.com/microsoft/vscode/issues/210467#issuecomment-2104566724 - UV_USE_IO_URING: 0 displayName: Run smoke tests (Electron) - script: yarn smoketest-no-compile --web --tracing --headless --electronArgs="--disable-dev-shm-usage" timeoutInMinutes: 20 env: - # TODO(deepak1556): Remove this once runtime is updated for - # https://github.com/microsoft/vscode/issues/210467#issuecomment-2104566724 + # TODO(deepak1556): Remove this once we update to Node.js >= 20.11.x UV_USE_IO_URING: 0 displayName: Run smoke tests (Browser, Chromium) - script: yarn smoketest-no-compile --remote --tracing timeoutInMinutes: 20 env: - # TODO(deepak1556): Remove this once runtime is updated for - # https://github.com/microsoft/vscode/issues/210467#issuecomment-2104566724 + # TODO(deepak1556): Remove this once we update to Node.js >= 20.11.x UV_USE_IO_URING: 0 displayName: Run smoke tests (Remote) - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: - script: yarn smoketest-no-compile --tracing --build "$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH)" timeoutInMinutes: 20 - env: - # TODO(deepak1556): Remove this once runtime is updated for - # https://github.com/microsoft/vscode/issues/210467#issuecomment-2104566724 - UV_USE_IO_URING: 0 displayName: Run smoke tests (Electron) - script: yarn smoketest-no-compile --web --tracing --headless --electronArgs="--disable-dev-shm-usage" env: VSCODE_REMOTE_SERVER_PATH: $(agent.builddirectory)/vscode-server-linux-$(VSCODE_ARCH)-web - # TODO(deepak1556): Remove this once runtime is updated for - # https://github.com/microsoft/vscode/issues/210467#issuecomment-2104566724 + # TODO(deepak1556): Remove this once we update to Node.js >= 20.11.x UV_USE_IO_URING: 0 timeoutInMinutes: 20 displayName: Run smoke tests (Browser, Chromium) @@ -221,8 +202,7 @@ steps: yarn smoketest-no-compile --tracing --remote --build "$APP_PATH" timeoutInMinutes: 20 env: - # TODO(deepak1556): Remove this once runtime is updated for - # https://github.com/microsoft/vscode/issues/210467#issuecomment-2104566724 + # TODO(deepak1556): Remove this once we update to Node.js >= 20.11.x UV_USE_IO_URING: 0 displayName: Run smoke tests (Remote) diff --git a/build/checksums/electron.txt b/build/checksums/electron.txt index 88fc9eceff0..a80aa1531f1 100644 --- a/build/checksums/electron.txt +++ b/build/checksums/electron.txt @@ -1,75 +1,75 @@ -e59378f63e935a6a561e272cdf44a8c5c3f4c56a8ff5ed0b33d45f18dd7d0d6c *chromedriver-v29.3.1-darwin-arm64.zip -4c4b2f11e9a396ff0e4c2282f4afe898f548af5e530a26c4c52fc7dbe307eb31 *chromedriver-v29.3.1-darwin-x64.zip -10b0d4a01636ae1f064cb950d5cff2a591dff2d2573fa9169335a492815169d3 *chromedriver-v29.3.1-linux-arm64.zip -45aff39d150dd423536d221bcdf2dab12cef4d0e8df50dddcda0387f60c70843 *chromedriver-v29.3.1-linux-armv7l.zip -569022d7a6fc4634ee4f496bb0414b7a8b34e505e22c2d423e915776e23d576a *chromedriver-v29.3.1-linux-x64.zip -d2090eb226eb0fef894837277d08a313af62da5807ab14d4aae7e6ba0a6a8466 *chromedriver-v29.3.1-mas-arm64.zip -3d425b6713d2a6e3149c4559cff76940e0443e236e61c7ba9b35ce2438f7de15 *chromedriver-v29.3.1-mas-x64.zip -5a95303fffbab24b07e842441e677ba98966dd800c90a7e842a97e43f7681cd6 *chromedriver-v29.3.1-win32-arm64.zip -553f8a81b0974c23eb473d5129450413f206e67128f89b7f7723ae76f9e8ec5d *chromedriver-v29.3.1-win32-ia32.zip -67f2f561703c6008c1c51dfd50be991752dfa3959bf5bb5a3a324143894fdcc5 *chromedriver-v29.3.1-win32-x64.zip -1e8366964ae298ec1e5e67b3f192c1d7a7cffc1b932b2f32fac3d075962c8f7e *electron-api.json -80596ef89f4638495bb24a92b75191bb0b61151e3cbc608090c1e406d14cafd5 *electron-v29.3.1-darwin-arm64-dsym-snapshot.zip -a2804d07dded66a5735aa1d1e5c547ea97bff09e2f1443c019ba564a33a5660b *electron-v29.3.1-darwin-arm64-dsym.zip -4dd9f6c00f2021dba34532452eecc15ce7e5eb914978319fd03246d19ff66baa *electron-v29.3.1-darwin-arm64-symbols.zip -aaada7a9f7ee72cd2a9a465ed0b8ec703aeedda9084f67cf72c1dce8e2aff7ca *electron-v29.3.1-darwin-arm64.zip -cab2c8a7a72c6e6b59e04e3292f27799b4a25592764960d9df4894fab405abc5 *electron-v29.3.1-darwin-x64-dsym-snapshot.zip -f7706f674d092f314fb30e85985d3172c1a125804e8132b206b65196b8dc81c5 *electron-v29.3.1-darwin-x64-dsym.zip -f00ec2929503e067b4ee59f8c38d1d2419db5c2af3c2b078d30d17faae8dbd5d *electron-v29.3.1-darwin-x64-symbols.zip -be6b70648d35959d346924e89aff5419af321c80f929d0e252fba131d9c93f50 *electron-v29.3.1-darwin-x64.zip -19f8b15ff1eb3a572adab73444c8b12f9815fa8ddaadbd8383ef5bb7370f98cc *electron-v29.3.1-linux-arm64-debug.zip -db0861e5d285428cc98de1f055fd7ef2fb2b331ebfd3e0a069bdf136b5bdc5c7 *electron-v29.3.1-linux-arm64-symbols.zip -d900a5597e296cb925dc2e6266b1d839b0254ab12e424d405785d6e351f1c4d7 *electron-v29.3.1-linux-arm64.zip -19f8b15ff1eb3a572adab73444c8b12f9815fa8ddaadbd8383ef5bb7370f98cc *electron-v29.3.1-linux-armv7l-debug.zip -8936bb96a59c1ac129555050ae00b478bbc6c16a0e759ed07231624b3ac52749 *electron-v29.3.1-linux-armv7l-symbols.zip -2a66d5603cf59a28699e4465488032f1dfac6118140ed129cf7403617329f983 *electron-v29.3.1-linux-armv7l.zip -a1f7984c302b2f7a03e836a7a6026d8ba64ca7806f47cd7b9dcc2e744680fd7c *electron-v29.3.1-linux-x64-debug.zip -fe2f5a78e7c485423fae7d204f6ba7bea95f9427703e97831a5555ab42ca93f3 *electron-v29.3.1-linux-x64-symbols.zip -d907e1c8074d2b7933d8b7525da3987f88d5b5ecf88131efec3eb5bd710a15b4 *electron-v29.3.1-linux-x64.zip -7cd32474a7c024d40ae9f17fe83678ae34b6f631889e895cabed87d0ee1781bf *electron-v29.3.1-mas-arm64-dsym-snapshot.zip -0058c71c614e252b4ad689de8a492563ebf938ba90cdb124e5454a9ec9d4c75c *electron-v29.3.1-mas-arm64-dsym.zip -aafedfab99d059079011139cf534bbfca44d50cd0a668b0ca547aeb8eed99c14 *electron-v29.3.1-mas-arm64-symbols.zip -f45417c845be012f0a9d3b8c92d5d3d5b4b9650e06809a9d783baa1ff8ce75a7 *electron-v29.3.1-mas-arm64.zip -8a91e7cced48162ec60368a992e8c53ef7e0ba574ce0c6edd8462167ba23b053 *electron-v29.3.1-mas-x64-dsym-snapshot.zip -4bfdd08bdb98afd3966e6a9c506dbb6e8bbbed7b0b22c7ae019b3cf8564e8354 *electron-v29.3.1-mas-x64-dsym.zip -dada1302a225509de9e031c8b139096a28398f883c8bdcae4b8fc3a92dc4c99d *electron-v29.3.1-mas-x64-symbols.zip -555d83c9eea2c1dc40c6996092ef2eb812d9d4937062d53d8909bf2a9432ca88 *electron-v29.3.1-mas-x64.zip -857bcb8f8866b2183355f71e968a690ec7d9ecc386b507988fe0ab560fe25a57 *electron-v29.3.1-win32-arm64-pdb.zip -a38ee738e44c3a9470ff765422410c59bd3c0a94a955e3b1ce661b68d094de18 *electron-v29.3.1-win32-arm64-symbols.zip -c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v29.3.1-win32-arm64-toolchain-profile.zip -74bcf7b7bb09c6311a5ef01eef41e20eccb84cd169651138234a332ad33aa087 *electron-v29.3.1-win32-arm64.zip -4650398c9c49b63050b4c2d28ae664c1d14912464a2744170338c131291aa290 *electron-v29.3.1-win32-ia32-pdb.zip -79a7a2db4c26c231d0963a924b129391cf920cd6b97d28ef095a2a1da4e14577 *electron-v29.3.1-win32-ia32-symbols.zip -c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v29.3.1-win32-ia32-toolchain-profile.zip -5527aa7d73b49d1c3298d9f2fc930be775e7d093a70bb613bec73e2ddd316afa *electron-v29.3.1-win32-ia32.zip -d85bc6393bd5890cf0bc616c41c2a5c0596ea4c3967d51bbf146a12cae727fad *electron-v29.3.1-win32-x64-pdb.zip -870bc19b8f38a84eb65fa6269fe2026b8dd8b76f9bbeded15c7313923f3e2c66 *electron-v29.3.1-win32-x64-symbols.zip -c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v29.3.1-win32-x64-toolchain-profile.zip -ccd465a085578168b6bf88ac76a5946f649e977efa7ef130c460b04df1becffa *electron-v29.3.1-win32-x64.zip -eab0311367f1e6b264ac788ac7291449d50bddf0049015391370a2eac462c320 *electron.d.ts -1718c59d8a963ef09325d300e10684d1a2419c186f0c70ac200d03b4142cdbfd *ffmpeg-v29.3.1-darwin-arm64.zip -a58339efba05ff93ca39e3000ec5aa5c81fb059c8786401324285defad11eb4b *ffmpeg-v29.3.1-darwin-x64.zip -4e2ba537d7c131abbd34168bce2c28cc9ef6262b217d5f4085afccfdf9635da6 *ffmpeg-v29.3.1-linux-arm64.zip -4aa56ad5d849f4e61af22678a179346b68aec9100282e1b8a43df25d95721677 *ffmpeg-v29.3.1-linux-armv7l.zip -0558e6e1f78229d303e16d4d8c290794baa9adc619fdd2ddccadb3ea241a1df4 *ffmpeg-v29.3.1-linux-x64.zip -a580fce86cd20aaee06ad4136b2dfbbf7a7449be8fc4d1a528535b2a83b067d4 *ffmpeg-v29.3.1-mas-arm64.zip -41d8a8d20429ea22bbfdd482f1ea2c265cabdca3cacb35737be01f427455204e *ffmpeg-v29.3.1-mas-x64.zip -ce3bf67555cf614c837d1bc80aaa071627750e3ffcde03a25b750796de23fd43 *ffmpeg-v29.3.1-win32-arm64.zip -f305313a1c3d15c6308c6158edc2d9cadc7465536adefa57fa31d879e6fe5e55 *ffmpeg-v29.3.1-win32-ia32.zip -8c6b7febbd80e53ea0cf0e89104006b4211b96d0d05933f517d69d0d578e8726 *ffmpeg-v29.3.1-win32-x64.zip -59827658661e330bc4ef876419c927a647a9c393aac2e7767887ae0ac600dd65 *hunspell_dictionaries.zip -1a5ec4216f0f938be6ae45853ffa032cfeb04409f757e9b66228362fe14da74f *libcxx-objects-v29.3.1-linux-arm64.zip -febbdade1c2958dc24498d62e6402bc1a9add49c3837487cc8ecb0ecb0f28459 *libcxx-objects-v29.3.1-linux-armv7l.zip -d6c5c7e67f8e50cad64493215b418a303cb5a30e39800c85d28401f66e1addb3 *libcxx-objects-v29.3.1-linux-x64.zip -57f87572e20185f329334ca9c6971bb7974424fb5ff4aa2e11f3a8668f8060f9 *libcxx_headers.zip -c7bcb0555dd10aed27ec7041338783df430e58da79ccba6863cbfb8cd89ef062 *libcxxabi_headers.zip -6962a9872def625e43d87ea4f50e90bc571d5b522abe7b9b33ce17714c43a05e *mksnapshot-v29.3.1-darwin-arm64.zip -b801394b60eb4cefe52fddd35616be9e79058b53d2860badb1daa0c61242b730 *mksnapshot-v29.3.1-darwin-x64.zip -906058eaeadb81f918962529185b2cf4b5fc6b13adbbf04fea7fd4ace9c1f20e *mksnapshot-v29.3.1-linux-arm64-x64.zip -74c9834b9d8237b001cbe5822b2426bebe910a72c1173377c1cd446f726bc2e7 *mksnapshot-v29.3.1-linux-armv7l-x64.zip -6eafdfcbdc44d48df267ddb09e09e0acda1abbe472211e947ce4b68852f99d52 *mksnapshot-v29.3.1-linux-x64.zip -04164d534fae6f12ad37e2f2268ec864b3c4417c08134edeeb555dd1bf73c073 *mksnapshot-v29.3.1-mas-arm64.zip -3a870add5f6c3287f9f958d0327f67d59ef0a9c30bfb94fb7155b6ee6e905e46 *mksnapshot-v29.3.1-mas-x64.zip -174ac9f2d8b4664587a983067c2b870b6e74fe8079715a502c11d55d774d6317 *mksnapshot-v29.3.1-win32-arm64-x64.zip -50efe4e272a54d04392ccd8a205164f4c1400c8197648d1a1fa71e667e37d7ff *mksnapshot-v29.3.1-win32-ia32.zip -e80d3e57ed67f05573a12a65b0be30cdb3e0b147cf817a80eb89b9886e7743ae *mksnapshot-v29.3.1-win32-x64.zip +3d3d8bb185d7b63b0db910661fdd69d6381afb8c97742bbd2526a9c932e1f8ca *chromedriver-v29.4.0-darwin-arm64.zip +c3d075943d87604ffa50382cc8d5798485349544ca391cab88c892f889d3b14c *chromedriver-v29.4.0-darwin-x64.zip +6d62d2dba55e4419fa003d45f93dad1324ec29a4d3eb84fd9fd5fd7a64339389 *chromedriver-v29.4.0-linux-arm64.zip +81bb3d362331c7296f700b1b0e8f07c4c7739b1151f698cd56af927bedda59e7 *chromedriver-v29.4.0-linux-armv7l.zip +ab593cc39aefac8c5abd259e31f6add4b2b70c52231724a6c08ac1872b4a0edf *chromedriver-v29.4.0-linux-x64.zip +705d42ccc05b2c48b0673b9dcf63eb78772bb79dba078a523d384ed2481bc9c0 *chromedriver-v29.4.0-mas-arm64.zip +956a7caa28eeeb0c02eb7638a53215ffd89b4f12880f0893ff10f497ca1a8117 *chromedriver-v29.4.0-mas-x64.zip +1f070176aa33e0139d61a3d758fd2f015f09bb275577293fe93564749b6310ba *chromedriver-v29.4.0-win32-arm64.zip +38a71526d243bcb73c28cb648bd4816d70b5e643df52f9f86a83416014589744 *chromedriver-v29.4.0-win32-ia32.zip +f90750d3589cb3c9f6f0ebc70d5e025cf81c382e8c23fa47a54570696a478ef0 *chromedriver-v29.4.0-win32-x64.zip +05dffc90dd1341cc7a6b50127985e4e217fef7f50a173c7d0ff34039dd2d81b6 *electron-api.json +7f63f7cf675ba6dec3a5e4173d729bd53c75f81e612f809641d9d0c4d9791649 *electron-v29.4.0-darwin-arm64-dsym-snapshot.zip +aa29530fcafa4db364978d4f414a6ec2005ea695f7fee70ffbe5e114e9e453f0 *electron-v29.4.0-darwin-arm64-dsym.zip +8d12fb6d9bcdf5bbfc93dbcd1cac348735dc6f98aa450ee03ec7837a01a8a938 *electron-v29.4.0-darwin-arm64-symbols.zip +c16d05f1231bb3c77da05ab236b454b3a2b6a642403be51e7c9b16cd2c421a19 *electron-v29.4.0-darwin-arm64.zip +2dfc1017831ab2f6e9ddb575d3b9cff5a0d56f16a335a3c0df508e964e2db963 *electron-v29.4.0-darwin-x64-dsym-snapshot.zip +025de6aa39d98762928e1b700f46177e74be20101b27457659b938e2c69db326 *electron-v29.4.0-darwin-x64-dsym.zip +ec4eb0a618207233985ceaab297be34b3d4f0813d88801d5637295b238dd661a *electron-v29.4.0-darwin-x64-symbols.zip +8ed7924f77a5c43c137a57097c5c47c2e8e9a78197e18af11a767c98035c123e *electron-v29.4.0-darwin-x64.zip +bde1772fa8ac4850e108012a9edd3bd93472bad8f68ddd55fca355dad81dde4f *electron-v29.4.0-linux-arm64-debug.zip +dfe7852a7423196efb2205c788d942db3ffc9de6ce52577e173bcf7ca6973d48 *electron-v29.4.0-linux-arm64-symbols.zip +c3764d6c3799950e3418e8e5a5a5b2c41abe421dd8bcdebf054c7c85798d9860 *electron-v29.4.0-linux-arm64.zip +bde1772fa8ac4850e108012a9edd3bd93472bad8f68ddd55fca355dad81dde4f *electron-v29.4.0-linux-armv7l-debug.zip +360668ba669cb2c01c2f960cdee76c29670e6ce907ccc0718e971a04af594ce9 *electron-v29.4.0-linux-armv7l-symbols.zip +c5e92943ad78b4e41a32ae53c679e148ea2ae09f95f914b1834dbdbae578ba91 *electron-v29.4.0-linux-armv7l.zip +375be885426bcbd272bd068bfcef41a83296c2f8e61e633233d2a9e9a69242fc *electron-v29.4.0-linux-x64-debug.zip +847e0f75624616c2918b33de2eefeec63419bd250685610d3f52fa115527d2b9 *electron-v29.4.0-linux-x64-symbols.zip +91e5eb374c2c85a07c2d4e99a89eb18515ff0169a49c3fa75289800e1225729e *electron-v29.4.0-linux-x64.zip +098f973537c3d9679a69409d0b84bcc1a6113bb2002ee60068e2c22f335a3855 *electron-v29.4.0-mas-arm64-dsym-snapshot.zip +2724aa32eb441eea21680d95fc1efdd75ac473fa19623c7acf3d546419e96154 *electron-v29.4.0-mas-arm64-dsym.zip +98dd81914752a57da4cbaad1f0aa94b16335f9b8f997be9aa049be90b96b2886 *electron-v29.4.0-mas-arm64-symbols.zip +fd2663f65c1f995304e3eb65870b7146adfefef07cf82bf44de75855fd4f36e8 *electron-v29.4.0-mas-arm64.zip +237983b2169e69bb73aa0987e871e3e486755904b71ebe36c3e902377f92754a *electron-v29.4.0-mas-x64-dsym-snapshot.zip +a5d59599827d32ef322b99eee8416e39235f4c7a0ada78342a885665e0b732dd *electron-v29.4.0-mas-x64-dsym.zip +5182e7697ac0591e0b95c33f70316af24093c9100f442be2cee0039660e959ac *electron-v29.4.0-mas-x64-symbols.zip +e0ee7057aff0240a70b9ed75ff44d55aeae9af67fbc8915f741711a8bb6fe744 *electron-v29.4.0-mas-x64.zip +2802872dfc6de0f0e2e8cef9d2f4f384e3d82b20ad36fc981c4e725dd2f2abcd *electron-v29.4.0-win32-arm64-pdb.zip +d49c954dc25ae9e4c75e61af80b9718014c52f016f43a29071913f0e7100c7bd *electron-v29.4.0-win32-arm64-symbols.zip +c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v29.4.0-win32-arm64-toolchain-profile.zip +483d692efbe4fb1231ff63afb8a236b2e22b486fbe5ac6abbc8b208abf94a4d3 *electron-v29.4.0-win32-arm64.zip +98458f49ba67a08e473d475a68a2818d9df076a5246fbc9b45403e8796f9d35b *electron-v29.4.0-win32-ia32-pdb.zip +69d505d4ae59d9dddf83c4e530e45dd7c5bc64d6da90cf4f851e523be9e51014 *electron-v29.4.0-win32-ia32-symbols.zip +c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v29.4.0-win32-ia32-toolchain-profile.zip +d5a21a17a64e9638f49f057356af23b51f56bd6a7fea3c2e0a28ff3186a7bc41 *electron-v29.4.0-win32-ia32.zip +521ee7b3398c4dc395b43dac86cd099e86a6123de2b43636ee805b7da014ed3f *electron-v29.4.0-win32-x64-pdb.zip +e33848ebd6c6e4ce431aa367bef887050947a136e883677cfc524ca5cabc1e98 *electron-v29.4.0-win32-x64-symbols.zip +c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v29.4.0-win32-x64-toolchain-profile.zip +e4ef85aa3608221f8a3e011c1b1c2d2d36093ad19bda12d16b3816929fb6c99b *electron-v29.4.0-win32-x64.zip +707ee08593289ee83514b4fc55123611309f995788f38a5ec03e285741aac1c8 *electron.d.ts +281b5f4a49de55fdb86b1662530f07f2ced1252c878eb7a941c88ede545339e0 *ffmpeg-v29.4.0-darwin-arm64.zip +0b735912df9b2ff3d03eb23942e03bc0116d82f1291d0a45cbde14177c2f3066 *ffmpeg-v29.4.0-darwin-x64.zip +4e2ba537d7c131abbd34168bce2c28cc9ef6262b217d5f4085afccfdf9635da6 *ffmpeg-v29.4.0-linux-arm64.zip +4aa56ad5d849f4e61af22678a179346b68aec9100282e1b8a43df25d95721677 *ffmpeg-v29.4.0-linux-armv7l.zip +0558e6e1f78229d303e16d4d8c290794baa9adc619fdd2ddccadb3ea241a1df4 *ffmpeg-v29.4.0-linux-x64.zip +224f15d8f96c75348cd7f1b85c4eab63468fae1e50ff4b1381e08011cf76e4f7 *ffmpeg-v29.4.0-mas-arm64.zip +175ec79f0dc4c5966d9a0ca6ec1674106340ecc64503585c12c2f854249af06f *ffmpeg-v29.4.0-mas-x64.zip +5fa13744b87fef1bfd24a37513677f446143e085504541f8ce97466803bd1893 *ffmpeg-v29.4.0-win32-arm64.zip +d7ba316bb7e13025c9db29e0acafebb540b7716c9f111e469733615d8521186a *ffmpeg-v29.4.0-win32-ia32.zip +35c70a28bcfd4f0b1f8c985d3d1348936bd60767d231ce28ba38f3daeeef64bb *ffmpeg-v29.4.0-win32-x64.zip +8c7228ea0ecab25a1f7fcd1ba9680684d19f9671a497113d71a851a53867b048 *hunspell_dictionaries.zip +7552547c8d585b9bc43518d239d7ce3ad7c5cad0346b07cdcfc1eab638b2b794 *libcxx-objects-v29.4.0-linux-arm64.zip +76054a779d4845ad752b625213ce8990f08dcc5b89aa20660dd4f2e817ba30a8 *libcxx-objects-v29.4.0-linux-armv7l.zip +761c317a9c874bd3d1118d0ecad33c4be23727f538cfbb42a08dd87c68da6039 *libcxx-objects-v29.4.0-linux-x64.zip +f98f9972cc30200b8e05815f5a9cd5cec04bdeee0e48ae2143cdaeff5db9d71d *libcxx_headers.zip +f0b0dd2be579baaf97901322ef489d03fae69a0b8524ea77b24fb3c896f73dd9 *libcxxabi_headers.zip +5da864ea23d70538298a40e0d037a5a461a6b74984e72fd4f0cd20904bccaed1 *mksnapshot-v29.4.0-darwin-arm64.zip +bde97bd7c69209ed6bf4cf1cdf7de622e3a9f50fe6b4dc4b5618eee868f47c62 *mksnapshot-v29.4.0-darwin-x64.zip +a3df9b9e6ef14efe5827d0256d8ecaebe6d8be130cfc3faac0dea76eb53b9b11 *mksnapshot-v29.4.0-linux-arm64-x64.zip +648b9dbca21194d663ddb706e6086a166e691263c764c80f836ae02c27e3657a *mksnapshot-v29.4.0-linux-armv7l-x64.zip +e7a4201cda3956380facc2b5b9d0b1020cc5e654fba44129fc7429a982411cc1 *mksnapshot-v29.4.0-linux-x64.zip +ffb44c45733675e0378f45fce25dafa95697d0c86179f8e46742ada16bc11aa1 *mksnapshot-v29.4.0-mas-arm64.zip +0242da3ca193206e56b88eb108502244bae35dcc587210bd0a32d9fa4cb71041 *mksnapshot-v29.4.0-mas-x64.zip +1445806dca6effbc60072bbde7997cefb62bdb7a9e295a090d26f27c3882685f *mksnapshot-v29.4.0-win32-arm64-x64.zip +09599adc3afb0a13ae87fc4b8ab97c729fe3689faa6a4f5f7a4a3cf0d9cc49d3 *mksnapshot-v29.4.0-win32-ia32.zip +84f80683d95665d29284386509bb104e840ff0b797bfbbd19da86b84d370aa49 *mksnapshot-v29.4.0-win32-x64.zip diff --git a/cgmanifest.json b/cgmanifest.json index a85c770cff2..f1e4192dc28 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -528,12 +528,12 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "384642792eb521b978a008ee1dbc30885edb7dcb" + "commitHash": "f9ed0eaee4b172733872c2f84e5061882dd08e5c" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "29.3.1" + "version": "29.4.0" }, { "component": { diff --git a/package.json b/package.json index 6b0467b4a14..b74ee912c95 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.90.0", - "distro": "b885c5b015796a5b6373decb919a391522135903", + "distro": "95d725e64e7e797849db840c27108f9f2e85a678", "author": { "name": "Microsoft Corporation" }, @@ -148,7 +148,7 @@ "cssnano": "^6.0.3", "debounce": "^1.0.0", "deemon": "^1.8.0", - "electron": "29.3.1", + "electron": "29.4.0", "eslint": "8.36.0", "eslint-plugin-header": "3.1.1", "eslint-plugin-jsdoc": "^46.5.0", diff --git a/remote/.yarnrc b/remote/.yarnrc index 60d35d09192..3a01071e2ba 100644 --- a/remote/.yarnrc +++ b/remote/.yarnrc @@ -1,5 +1,5 @@ disturl "https://nodejs.org/dist" target "20.9.0" -ms_build_id "267516" +ms_build_id "274207" runtime "node" build_from_source "true" diff --git a/resources/server/bin/code-server-linux.sh b/resources/server/bin/code-server-linux.sh index 3d8881ee601..9229ec89a0e 100644 --- a/resources/server/bin/code-server-linux.sh +++ b/resources/server/bin/code-server-linux.sh @@ -9,6 +9,8 @@ esac ROOT="$(dirname "$(dirname "$(readlink -f "$0")")")" -export UV_USE_IO_URING=0 # workaround for https://github.com/microsoft/vscode/issues/212678 +# workaround for https://github.com/microsoft/vscode/issues/212678 +# Remove this once we update to Node.js >= 20.11.x +export UV_USE_IO_URING=0 "$ROOT/node" ${INSPECT:-} "$ROOT/out/server-main.js" "$@" diff --git a/src/vs/platform/dialogs/electron-main/dialogMainService.ts b/src/vs/platform/dialogs/electron-main/dialogMainService.ts index 666829d56be..bc1230a48ea 100644 --- a/src/vs/platform/dialogs/electron-main/dialogMainService.ts +++ b/src/vs/platform/dialogs/electron-main/dialogMainService.ts @@ -155,7 +155,7 @@ export class DialogMainService implements IDialogMainService { if (!fileDialogLock) { this.logService.error('[DialogMainService]: file save dialog is already or will be showing for the window with the same configuration'); - return { canceled: true }; + return { canceled: true, filePath: '' }; } try { diff --git a/yarn.lock b/yarn.lock index ac573d403b4..1d866e83a1f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3845,10 +3845,10 @@ electron-to-chromium@^1.4.668: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.717.tgz#99db370cae8cd090d5b01f8748e9ad369924d0f8" integrity sha512-6Fmg8QkkumNOwuZ/5mIbMU9WI3H2fmn5ajcVya64I5Yr5CcNmO7vcLt0Y7c96DCiMO5/9G+4sI2r6eEvdg1F7A== -electron@29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/electron/-/electron-29.3.1.tgz#87c82b2cd2c326f78f036499377a5448bea5d4bb" - integrity sha512-auge1/6RVqgUd6TgIq88wKdUCJi2cjESi3jy7d+6X4JzvBGprKBqMJ8JSSFpu/Px1YJrFUKAxfy6SC+TQf1uLw== +electron@29.4.0: + version "29.4.0" + resolved "https://registry.yarnpkg.com/electron/-/electron-29.4.0.tgz#5dcd5a977414337a2518619e9166c0e86a5a3bae" + integrity sha512-4DTO8U66oiI8rShrDSu2zDPW6GWRiCebyb1MHSfQkLWCNI/PnLyGKeqYPUoVgc0FWaNN2sCBn8NKJHb++hE2LQ== dependencies: "@electron/get" "^2.0.0" "@types/node" "^20.9.0" From eb99b85bdf8bdcd64b917cf99d096e40e80cff1f Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Mon, 20 May 2024 08:28:13 -0700 Subject: [PATCH 272/357] chore: update CLI dependences, add env var options for login --- cli/Cargo.lock | 1601 +++++++++-------- cli/Cargo.toml | 4 +- cli/src/commands/args.rs | 4 +- cli/src/rpc.rs | 1 + extensions/tunnel-forwarding/src/extension.ts | 4 +- .../remoteTunnel/node/remoteTunnelService.ts | 8 +- 6 files changed, 863 insertions(+), 759 deletions(-) diff --git a/cli/Cargo.lock b/cli/Cargo.lock index 4d62806a93c..3be3815a748 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + [[package]] name = "adler" version = "1.0.2" @@ -10,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aho-corasick" -version = "1.0.1" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] @@ -34,51 +43,51 @@ dependencies = [ [[package]] name = "anstream" -version = "0.3.2" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", - "is-terminal", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.0" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" [[package]] name = "anstyle-parse" -version = "0.2.0" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "1.0.1" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" dependencies = [ "anstyle", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -87,113 +96,176 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c48ccdbf6ca6b121e0f586cbc0e73ae440e56c67c30fa0873b4e110d9c26d2b" dependencies = [ - "event-listener", + "event-listener 2.5.3", "futures-core", ] [[package]] name = "async-channel" -version = "1.8.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" dependencies = [ - "concurrent-queue 2.2.0", - "event-listener", + "concurrent-queue", + "event-listener-strategy 0.5.2", "futures-core", + "pin-project-lite", ] [[package]] name = "async-io" -version = "1.9.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83e21f3a490c72b3b0cf44962180e60045de2925d8dff97918f7ee43c8f637c7" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" dependencies = [ + "async-lock 2.8.0", "autocfg", - "concurrent-queue 1.2.4", - "futures-lite", - "libc", + "cfg-if", + "concurrent-queue", + "futures-lite 1.13.0", "log", - "once_cell", "parking", - "polling", + "polling 2.8.0", + "rustix 0.37.27", "slab", - "socket2", + "socket2 0.4.10", "waker-fn", - "winapi", +] + +[[package]] +name = "async-io" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcccb0f599cfa2f8ace422d3555572f47424da5648a4382a9dd0310ff8210884" +dependencies = [ + "async-lock 3.3.0", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite 2.3.0", + "parking", + "polling 3.7.0", + "rustix 0.38.34", + "slab", + "tracing", + "windows-sys 0.52.0", ] [[package]] name = "async-lock" -version = "2.7.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa24f727524730b077666307f2734b4a1a1c57acb79193127dcc8914d5242dd7" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" dependencies = [ - "event-listener", + "event-listener 2.5.3", +] + +[[package]] +name = "async-lock" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b" +dependencies = [ + "event-listener 4.0.3", + "event-listener-strategy 0.4.0", + "pin-project-lite", ] [[package]] name = "async-process" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9d28b1d97e08915212e2e45310d47854eafa69600756fc735fb788f75199c9" +checksum = "ea6438ba0a08d81529c69b36700fa2f95837bfe3e776ab39cde9c14d9149da88" dependencies = [ - "async-io", - "async-lock", - "autocfg", + "async-io 1.13.0", + "async-lock 2.8.0", + "async-signal", "blocking", "cfg-if", - "event-listener", - "futures-lite", - "rustix", - "signal-hook", + "event-listener 3.1.0", + "futures-lite 1.13.0", + "rustix 0.38.34", "windows-sys 0.48.0", ] [[package]] name = "async-recursion" -version = "1.0.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cda8f4bcc10624c4e85bc66b3f452cca98cfa5ca002dc83a16aad2367641bea" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 1.0.103", + "syn 2.0.65", +] + +[[package]] +name = "async-signal" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afe66191c335039c7bb78f99dc7520b0cbb166b3a1cb33a03f53d8a1c6f2afda" +dependencies = [ + "async-io 2.3.2", + "async-lock 3.3.0", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 0.38.34", + "signal-hook-registry", + "slab", + "windows-sys 0.52.0", ] [[package]] name = "async-task" -version = "4.4.0" +version = "4.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc7ab41815b3c653ccd2978ec3255c81349336702dfdf62ee6f7069b12a3aae" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.68" +version = "0.1.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" +checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.65", ] [[package]] name = "atomic-waker" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.1.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "backtrace" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] [[package]] name = "base64" -version = "0.21.2" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "bit-vec" @@ -209,72 +281,65 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" [[package]] name = "block-buffer" -version = "0.10.3" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "block-padding" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a90ec2df9600c28a01c56c4784c9207a96d2451833aeceb8cc97e4c9548bb78" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" dependencies = [ "generic-array", ] [[package]] name = "blocking" -version = "1.3.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77231a1c8f801696fc0123ec6150ce92cffb8e164a02afb9c8ddee0e9b65ad65" +checksum = "495f7104e962b7356f0aeb34247aca1fe7d2e783b346582db7f2904cb5717e88" dependencies = [ "async-channel", - "async-lock", + "async-lock 3.3.0", "async-task", - "atomic-waker", - "fastrand", - "futures-lite", - "log", + "futures-io", + "futures-lite 2.3.0", + "piper", ] [[package]] name = "bumpalo" -version = "3.12.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.4.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" - -[[package]] -name = "cache-padded" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "cc" -version = "1.0.73" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" [[package]] name = "cfg-if" @@ -284,58 +349,56 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.26" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", "serde", - "winapi", + "windows-targets 0.52.5", ] [[package]] name = "clap" -version = "4.3.0" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93aae7a4192245f70fe75dd9157fc7b4a5bf53e88d30bd4396f7d8f9284d5acc" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" dependencies = [ "clap_builder", "clap_derive", - "once_cell", ] [[package]] name = "clap_builder" -version = "4.3.0" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f423e341edefb78c9caba2d9c7f7687d0e72e89df3ce3394554754393ac3990" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" dependencies = [ "anstream", "anstyle", - "bitflags 1.3.2", "clap_lex", "strsim", ] [[package]] name = "clap_derive" -version = "4.3.0" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "191d9573962933b4027f932c600cd252ce27a8ad5979418fe78e43c07996f27b" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.65", ] [[package]] name = "clap_lex" -version = "0.5.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" [[package]] name = "code-cli" @@ -389,67 +452,48 @@ dependencies = [ "zip", ] -[[package]] -name = "codespan-reporting" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" -dependencies = [ - "termcolor", - "unicode-width", -] - [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" [[package]] name = "concurrent-queue" -version = "1.2.4" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af4780a44ab5696ea9e28294517f1fffb421a83a25af521333c838635509db9c" -dependencies = [ - "cache-padded", -] - -[[package]] -name = "concurrent-queue" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ "crossbeam-utils", ] [[package]] name = "console" -version = "0.15.7" +version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" dependencies = [ "encode_unicode", "lazy_static", "libc", "unicode-width", - "windows-sys 0.45.0", + "windows-sys 0.52.0", ] [[package]] name = "const_format" -version = "0.2.31" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c990efc7a285731f9a4378d81aff2f0e85a2c8781a05ef0f8baa8dac54d0ff48" +checksum = "e3a214c7af3d04997541b18d432afaff4c455e79e2029079647e72fc2bd27673" dependencies = [ "const_format_proc_macros", ] [[package]] name = "const_format_proc_macros" -version = "0.2.31" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e026b6ce194a874cb9cf32cd5772d1ef9767cc8fcb5765948d74f37a9d8b2bf6" +checksum = "c7f6ff08fd20f4f299298a28e2dfa8a8ba1036e6cd2460ac1de7b425d76f2500" dependencies = [ "proc-macro2", "quote", @@ -458,9 +502,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", @@ -468,42 +512,42 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" -version = "0.2.8" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03e69e28e9f7f77debdedbaafa2866e1de9ba56df55a8bd7cfc724c25a09987c" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] [[package]] name = "crc32fast" -version = "1.3.2" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "58ebf8d6963185c7625d2c3c3962d99eb8936637b1427536d21dc36ae402ebad" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-channel" -version = "0.5.12" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crypto-common" @@ -515,55 +559,20 @@ dependencies = [ "typenum", ] -[[package]] -name = "cxx" -version = "1.0.97" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88abab2f5abbe4c56e8f1fb431b784d710b709888f35755a160e62e33fe38e8" -dependencies = [ - "cc", - "cxxbridge-flags", - "cxxbridge-macro", - "link-cplusplus", -] - -[[package]] -name = "cxx-build" -version = "1.0.97" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c0c11acd0e63bae27dcd2afced407063312771212b7a823b4fd72d633be30fb" -dependencies = [ - "cc", - "codespan-reporting", - "once_cell", - "proc-macro2", - "quote", - "scratch", - "syn 2.0.18", -] - -[[package]] -name = "cxxbridge-flags" -version = "1.0.97" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d3816ed957c008ccd4728485511e3d9aaf7db419aa321e3d2c5a2f3411e36c8" - -[[package]] -name = "cxxbridge-macro" -version = "1.0.97" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26acccf6f445af85ea056362561a24ef56cdc15fcc685f03aec50b9c702cb6d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.18", -] - [[package]] name = "data-encoding" -version = "2.3.2" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] [[package]] name = "derivative" @@ -573,7 +582,7 @@ checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ "proc-macro2", "quote", - "syn 1.0.103", + "syn 1.0.109", ] [[package]] @@ -590,9 +599,9 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.5" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", @@ -648,18 +657,18 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "encoding_rs" -version = "0.8.31" +version = "0.8.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" dependencies = [ "cfg-if", ] [[package]] name = "enumflags2" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c041f5090df68b32bcd905365fd51769c8b9d553fe87fde0b683534f10c01bd2" +checksum = "3278c9d5fb675e0a51dabcf4c0d355f692b064171535ba72361be1528a9d8e8d" dependencies = [ "enumflags2_derive", "serde", @@ -667,13 +676,13 @@ dependencies = [ [[package]] name = "enumflags2_derive" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e9a1f9f7d83e59740248a6e14ecf93929ade55027844dfcea78beafccc15745" +checksum = "5c785274071b1b420972453b306eeca06acf4633829db4223b58a2a8c5953bc4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.65", ] [[package]] @@ -684,23 +693,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.1" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ - "errno-dragonfly", - "libc", - "windows-sys 0.48.0", -] - -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", "libc", + "windows-sys 0.52.0", ] [[package]] @@ -710,31 +708,90 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] -name = "fastrand" -version = "1.8.0" +name = "event-listener" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +checksum = "d93877bcde0eb80ca09131a08d23f0a5c18a620b01db137dba666d18cd9b30c2" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9944b8ca13534cdfb2800775f8dd4902ff3fc75a50101466decadfdf322a24" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" +dependencies = [ + "event-listener 4.0.3", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +dependencies = [ + "event-listener 5.3.0", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" dependencies = [ "instant", ] [[package]] -name = "filetime" -version = "0.2.17" +name = "fastrand" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94a7bbaa59354bc20dd75b67f23e2797b4490e9d6928203fb105c79e448c86c" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" + +[[package]] +name = "filetime" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.2.16", - "windows-sys 0.36.1", + "redox_syscall 0.4.1", + "windows-sys 0.52.0", ] [[package]] name = "flate2" -version = "1.0.26" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", "libz-sys", @@ -764,18 +821,18 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] [[package]] name = "futures" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", @@ -788,9 +845,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", @@ -798,15 +855,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ "futures-core", "futures-task", @@ -815,17 +872,17 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-lite" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" dependencies = [ - "fastrand", + "fastrand 1.9.0", "futures-core", "futures-io", "memchr", @@ -835,33 +892,43 @@ dependencies = [ ] [[package]] -name = "futures-macro" -version = "0.3.28" +name = "futures-lite" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.65", ] [[package]] name = "futures-sink" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", @@ -877,9 +944,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.6" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -892,7 +959,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" dependencies = [ "libc", - "windows-targets 0.48.0", + "windows-targets 0.48.5", ] [[package]] @@ -908,15 +975,21 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.7" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", "wasi 0.11.0+wasi-snapshot-preview1", ] +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + [[package]] name = "h2" version = "0.3.26" @@ -929,7 +1002,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 2.1.0", + "indexmap 2.2.6", "slab", "tokio", "tokio-util", @@ -944,30 +1017,21 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "heck" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.1.19" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - -[[package]] -name = "hermit-abi" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hex" @@ -992,9 +1056,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.9" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ "bytes", "fnv", @@ -1003,9 +1067,9 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", "http", @@ -1020,15 +1084,15 @@ checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "0.14.26" +version = "0.14.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" dependencies = [ "bytes", "futures-channel", @@ -1041,7 +1105,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.5.7", "tokio", "tower-service", "tracing", @@ -1063,33 +1127,32 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.57" +version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows", + "windows-core", ] [[package]] name = "iana-time-zone-haiku" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ - "cxx", - "cxx-build", + "cc", ] [[package]] name = "idna" -version = "0.3.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -1107,19 +1170,19 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.1.0" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown 0.14.3", + "hashbrown 0.14.5", ] [[package]] name = "indicatif" -version = "0.17.4" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db45317f37ef454e6519b6c3ed7d377e5f23346f0823f86e65ca36912d1d0ef8" +checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" dependencies = [ "console", "instant", @@ -1140,9 +1203,9 @@ dependencies = [ [[package]] name = "instant" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ "cfg-if", ] @@ -1153,16 +1216,16 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ - "hermit-abi 0.3.1", + "hermit-abi", "libc", "windows-sys 0.48.0", ] [[package]] name = "ipnet" -version = "2.5.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] name = "is-docker" @@ -1173,18 +1236,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "is-terminal" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" -dependencies = [ - "hermit-abi 0.3.1", - "io-lifetimes", - "rustix", - "windows-sys 0.48.0", -] - [[package]] name = "is-wsl" version = "0.4.0" @@ -1196,32 +1247,38 @@ dependencies = [ ] [[package]] -name = "itoa" -version = "1.0.4" +name = "is_terminal_polyfill" +version = "1.70.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "js-sys" -version = "0.3.60" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] [[package]] name = "keyring" -version = "2.0.3" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e319fe0cb5b29a55cdb228df3f651b6c8cdc5b19520f3e62c8f111dc2582026c" +checksum = "363387f0019d714aa60cc30ab4fe501a747f4c08fc58f069dd14be971bd495a0" dependencies = [ "byteorder", "lazy_static", "linux-keyutils", "secret-service", "security-framework", - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -1232,37 +1289,38 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.153" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.5.0", + "libc", +] [[package]] name = "libz-sys" -version = "1.1.12" +version = "1.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d97137b25e321a73eef1418d1d5d2eda4d77e12813f8e6dead84bc52c5870a7b" +checksum = "5e143b5e666b2695d28f6bca6497720813f699c9602dd7f5cac91008b8ada7f9" dependencies = [ "cc", "pkg-config", "vcpkg", ] -[[package]] -name = "link-cplusplus" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d240c6f7e1ba3a28b0249f774e6a9dd0175054b52dfbb61b16eb8505c3785c9" -dependencies = [ - "cc", -] - [[package]] name = "linux-keyutils" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f27bb67f6dd1d0bb5ab582868e4f65052e58da6401188a08f0da09cf512b84b" +checksum = "761e49ec5fd8a5a463f9b84e877c373d888935b71c6be78f3767fe2ae6bed18e" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.5.0", "libc", ] @@ -1273,10 +1331,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] -name = "lock_api" -version = "0.4.9" +name = "linux-raw-sys" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -1284,9 +1348,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.18" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "md5" @@ -1296,9 +1360,9 @@ checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" [[package]] name = "memchr" -version = "2.5.0" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "memoffset" @@ -1310,16 +1374,25 @@ dependencies = [ ] [[package]] -name = "mime" -version = "0.3.16" +name = "memoffset" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" dependencies = [ "adler", ] @@ -1355,31 +1428,30 @@ dependencies = [ [[package]] name = "nix" -version = "0.26.2" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" dependencies = [ "bitflags 1.3.2", "cfg-if", "libc", - "memoffset", - "static_assertions", + "memoffset 0.7.1", ] [[package]] name = "ntapi" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc51db7b362b205941f71232e56c625156eb9a929f8cf74a428fd5bc094a4afc" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" dependencies = [ "winapi", ] [[package]] name = "num" -version = "0.4.0" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" dependencies = [ "num-bigint", "num-complex", @@ -1391,11 +1463,10 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.3" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" dependencies = [ - "autocfg", "num-integer", "num-traits", "rand 0.8.5", @@ -1403,28 +1474,33 @@ dependencies = [ [[package]] name = "num-complex" -version = "0.4.2" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ae39348c8bc5fbd7f40c727a9925f03517afd2ab27d46702108b6a7e5414c19" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" dependencies = [ "num-traits", ] [[package]] -name = "num-integer" -version = "0.1.45" +name = "num-conv" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ - "autocfg", "num-traits", ] [[package]] name = "num-iter" -version = "0.1.43" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" dependencies = [ "autocfg", "num-integer", @@ -1433,11 +1509,10 @@ dependencies = [ [[package]] name = "num-rational" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" dependencies = [ - "autocfg", "num-bigint", "num-integer", "num-traits", @@ -1445,20 +1520,20 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "num_cpus" -version = "1.13.1" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.1.19", + "hermit-abi", "libc", ] @@ -1469,28 +1544,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] -name = "once_cell" -version = "1.17.2" +name = "object" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9670a07f94779e00908f3e686eab508878ebb390ba6e604d3a284c00e8d0487b" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "open" -version = "4.1.0" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16814a067484415fda653868c9be0ac5f2abd2ef5d951082a5f2fe1b3662944" +checksum = "3a083c0c7e5e4a8ec4176346cf61f67ac674e8bfb059d9226e1c54a96b377c12" dependencies = [ "is-wsl", + "libc", "pathdiff", ] [[package]] name = "openssl" -version = "0.10.60" +version = "0.10.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79a4c6c3a2b158f7f8f2a2fc5a969fa3a068df6fc9dbb4a43845436e3af7c800" +checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.5.0", "cfg-if", "foreign-types", "libc", @@ -1501,13 +1586,13 @@ dependencies = [ [[package]] name = "openssl-macros" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 1.0.103", + "syn 2.0.65", ] [[package]] @@ -1518,9 +1603,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.96" +version = "0.9.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3812c071ba60da8b5677cc12bcb1d42989a65553772897a7e0355545a819838f" +checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" dependencies = [ "cc", "libc", @@ -1591,25 +1676,25 @@ dependencies = [ [[package]] name = "os_info" -version = "3.7.0" +version = "3.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "006e42d5b888366f1880eda20371fedde764ed2213dc8496f49622fa0c99cd5e" +checksum = "ae99c7fa6dd38c7cafe1ec085e804f8f555a2f8659b0dbe03f1f9963a9b51092" dependencies = [ "log", - "winapi", + "windows-sys 0.52.0", ] [[package]] name = "parking" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" dependencies = [ "lock_api", "parking_lot_core", @@ -1617,22 +1702,22 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.3" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.2.16", + "redox_syscall 0.5.1", "smallvec", - "windows-sys 0.36.1", + "windows-targets 0.52.5", ] [[package]] name = "paste" -version = "1.0.9" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1de2e551fb905ac83f73f7aedf2f0cb4a0da7e35efa24a202a936269f1f18e1" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pathdiff" @@ -1642,35 +1727,35 @@ checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" [[package]] name = "percent-encoding" -version = "2.2.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project" -version = "1.1.0" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c95a7476719eab1e366eaf73d0260af3021184f18177925b07f54b30089ceead" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.0" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.65", ] [[package]] name = "pin-project-lite" -version = "0.2.9" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -1679,62 +1764,95 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] -name = "pkg-config" -version = "0.3.25" +name = "piper" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" +checksum = "464db0c665917b13ebb5d453ccdec4add5658ee1adc7affc7677615356a8afaf" +dependencies = [ + "atomic-waker", + "fastrand 2.1.0", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "polling" -version = "2.3.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899b00b9c8ab553c743b3e11e87c5c7d423b2a2de229ba95b24a756344748011" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" dependencies = [ "autocfg", + "bitflags 1.3.2", "cfg-if", + "concurrent-queue", "libc", "log", - "wepoll-ffi", - "winapi", + "pin-project-lite", + "windows-sys 0.48.0", +] + +[[package]] +name = "polling" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645493cf344456ef24219d02a768cf1fb92ddf8c92161679ae3d91b91a637be3" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 0.38.34", + "tracing", + "windows-sys 0.52.0", ] [[package]] name = "portable-atomic" -version = "1.3.3" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "767eb9f07d4a5ebcb39bbf2d452058a93c011373abf6832e24194a1c3f004794" +checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro-crate" -version = "1.2.1" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ "once_cell", - "thiserror", - "toml", + "toml_edit", ] [[package]] name = "proc-macro2" -version = "1.0.80" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56dea16b0a29e94408b9aa5e2940a4eedbd128a1ba20e8f7ae60fd3d465af0e" +checksum = "0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.28" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -1798,7 +1916,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.7", + "getrandom 0.2.15", ] [[package]] @@ -1812,38 +1930,50 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.16" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "redox_syscall" -version = "0.3.5" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.5.0", ] [[package]] name = "redox_users" -version = "0.4.3" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" dependencies = [ - "getrandom 0.2.7", - "redox_syscall 0.2.16", + "getrandom 0.2.15", + "libredox", "thiserror", ] [[package]] name = "regex" -version = "1.8.3" +version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81ca098a9821bd52d6b24fd8b10bd081f47d39c22778cafaa75a2857a62c6390" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" dependencies = [ "aho-corasick", "memchr", @@ -1852,15 +1982,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.7.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "reqwest" -version = "0.11.22" +version = "0.11.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ "base64", "bytes", @@ -1880,9 +2010,11 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", + "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", + "sync_wrapper", "system-configuration", "tokio", "tokio-native-tls", @@ -1898,9 +2030,9 @@ dependencies = [ [[package]] name = "rmp" -version = "0.8.11" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44519172358fd6d58656c86ab8e7fbc9e1490c3e8f14d35ed78ca0dd07403c9f" +checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" dependencies = [ "byteorder", "num-traits", @@ -1909,9 +2041,9 @@ dependencies = [ [[package]] name = "rmp-serde" -version = "1.1.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5b13be192e0220b8afb7222aa5813cb62cc269ebb5cac346ca6487681d2913e" +checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" dependencies = [ "byteorder", "rmp", @@ -1921,7 +2053,7 @@ dependencies = [ [[package]] name = "russh" version = "0.37.1" -source = "git+https://github.com/microsoft/vscode-russh?branch=main#6a15199c784c0b6d171a6fec09ed730a5cd1350d" +source = "git+https://github.com/microsoft/vscode-russh?branch=main#fd4f608a83753f9f3e137f95600faffede71cf65" dependencies = [ "async-trait", "bitflags 1.3.2", @@ -1950,7 +2082,7 @@ dependencies = [ [[package]] name = "russh-cryptovec" version = "0.7.0" -source = "git+https://github.com/microsoft/vscode-russh?branch=main#6a15199c784c0b6d171a6fec09ed730a5cd1350d" +source = "git+https://github.com/microsoft/vscode-russh?branch=main#fd4f608a83753f9f3e137f95600faffede71cf65" dependencies = [ "libc", "winapi", @@ -1959,7 +2091,7 @@ dependencies = [ [[package]] name = "russh-keys" version = "0.37.1" -source = "git+https://github.com/microsoft/vscode-russh?branch=main#6a15199c784c0b6d171a6fec09ed730a5cd1350d" +source = "git+https://github.com/microsoft/vscode-russh?branch=main#fd4f608a83753f9f3e137f95600faffede71cf65" dependencies = [ "bit-vec", "byteorder", @@ -1984,46 +2116,67 @@ dependencies = [ ] [[package]] -name = "rustix" -version = "0.37.25" +name = "rustc-demangle" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4eb579851244c2c03e7c24f501c3432bed80b8f720af1d6e5b0e0f01555a035" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustix" +version = "0.37.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" dependencies = [ "bitflags 1.3.2", "errno", "io-lifetimes", "libc", - "linux-raw-sys", + "linux-raw-sys 0.3.8", "windows-sys 0.48.0", ] [[package]] -name = "ryu" -version = "1.0.11" +name = "rustix" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags 2.5.0", + "errno", + "libc", + "linux-raw-sys 0.4.14", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "schannel" -version = "0.1.20" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" dependencies = [ - "lazy_static", - "windows-sys 0.36.1", + "windows-sys 0.52.0", ] [[package]] name = "scopeguard" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" - -[[package]] -name = "scratch" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "secret-service" @@ -2043,11 +2196,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.7.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c" +checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.5.0", "core-foundation", "core-foundation-sys", "libc", @@ -2056,9 +2209,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.6.1" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" dependencies = [ "core-foundation-sys", "libc", @@ -2066,38 +2219,38 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.163" +version = "1.0.202" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" +checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" dependencies = [ "serde_derive", ] [[package]] name = "serde_bytes" -version = "0.11.9" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416bda436f9aab92e02c8e10d49a15ddd339cea90b6e340fe51ed97abb548294" +checksum = "8b8497c313fd43ab992087548117643f6fcd935cbf36f176ffda0aacf9591734" dependencies = [ "serde", ] [[package]] name = "serde_derive" -version = "1.0.163" +version = "1.0.202" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" +checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.65", ] [[package]] name = "serde_json" -version = "1.0.96" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" dependencies = [ "itoa", "ryu", @@ -2106,13 +2259,13 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.9" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fe39d9fbb0ebf5eb2c7cb7e2a47e4f462fad1379f1166b8ae49ad9eae89a7ca" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 1.0.103", + "syn 2.0.65", ] [[package]] @@ -2129,9 +2282,9 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", @@ -2140,9 +2293,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.6" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", @@ -2161,50 +2314,50 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" -[[package]] -name = "signal-hook" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9" -dependencies = [ - "libc", - "signal-hook-registry", -] - [[package]] name = "signal-hook-registry" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] [[package]] name = "slab" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] [[package]] name = "smallvec" -version = "1.10.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" dependencies = [ "libc", "winapi", ] +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -2213,21 +2366,21 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "strsim" -version = "0.10.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "subtle" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "syn" -version = "1.0.103" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", @@ -2236,20 +2389,26 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.18" +version = "2.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" +checksum = "d2863d96a84c6439701d7a38f9de935ec562c8832cc55d1dde0f513b52fad106" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + [[package]] name = "sysinfo" -version = "0.29.0" +version = "0.29.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02f1dc6930a439cc5d154221b5387d153f8183529b07c19aca24ea31e0a167e1" +checksum = "cd727fc423c2060f6c92d9534cef765c65a6ed3f428a03d7def74a8c4348e666" dependencies = [ "cfg-if", "core-foundation-sys", @@ -2282,9 +2441,9 @@ dependencies = [ [[package]] name = "tar" -version = "0.4.38" +version = "0.4.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b55807c0344e1e6c04d7c965f5289c39a8d94ae23ed5c0b57aabac549f871c6" +checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb" dependencies = [ "filetime", "libc", @@ -2293,61 +2452,54 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.5.0" +version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", - "fastrand", - "redox_syscall 0.3.5", - "rustix", - "windows-sys 0.45.0", -] - -[[package]] -name = "termcolor" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" -dependencies = [ - "winapi-util", + "fastrand 2.1.0", + "rustix 0.38.34", + "windows-sys 0.52.0", ] [[package]] name = "thiserror" -version = "1.0.40" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.40" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.65", ] [[package]] name = "time" -version = "0.3.21" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ + "deranged", + "num-conv", + "powerfmt", "serde", "time-core", ] [[package]] name = "time-core" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "tinyvec" @@ -2360,17 +2512,17 @@ dependencies = [ [[package]] name = "tinyvec_macros" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.28.2" +version = "1.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94d7b1cfd2aa4011f2de74c2c4c63665e27a71006b0a192dcd2710272e73dfa2" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" dependencies = [ - "autocfg", + "backtrace", "bytes", "libc", "mio", @@ -2378,7 +2530,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.5.7", "tokio-macros", "tracing", "windows-sys 0.48.0", @@ -2386,13 +2538,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.65", ] [[package]] @@ -2407,9 +2559,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.11" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d660770404473ccd7bc9f8b28494a811bc18542b915c0855c51e8f419d5223ce" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" dependencies = [ "futures-core", "pin-project-lite", @@ -2432,9 +2584,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.8" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", @@ -2442,16 +2594,23 @@ dependencies = [ "futures-sink", "pin-project-lite", "tokio", - "tracing", ] [[package]] -name = "toml" -version = "0.5.9" +name = "toml_datetime" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "serde", + "indexmap 2.2.6", + "toml_datetime", + "winnow", ] [[package]] @@ -2462,11 +2621,10 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.37" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "cfg-if", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -2474,29 +2632,29 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.23" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 1.0.103", + "syn 2.0.65", ] [[package]] name = "tracing-core" -version = "0.1.30" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", ] [[package]] name = "try-lock" -version = "0.2.3" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tungstenite" @@ -2548,46 +2706,47 @@ dependencies = [ [[package]] name = "typenum" -version = "1.15.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "uds_windows" -version = "1.0.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce65604324d3cce9b966701489fbd0cf318cb1f7bd9dd07ac9a4ee6fb791930d" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" dependencies = [ + "memoffset 0.9.1", "tempfile", "winapi", ] [[package]] name = "unicode-bidi" -version = "0.3.8" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" -version = "1.0.5" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" dependencies = [ "tinyvec", ] [[package]] name = "unicode-width" -version = "0.1.10" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" [[package]] name = "unicode-xid" @@ -2597,9 +2756,9 @@ checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" [[package]] name = "url" -version = "2.3.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", "idna", @@ -2626,11 +2785,11 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uuid" -version = "1.4.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" +checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" dependencies = [ - "getrandom 0.2.7", + "getrandom 0.2.15", "serde", ] @@ -2648,17 +2807,16 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "waker-fn" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" [[package]] name = "want" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ - "log", "try-lock", ] @@ -2676,9 +2834,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.83" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -2686,24 +2844,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.83" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 1.0.103", + "syn 2.0.65", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.33" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" dependencies = [ "cfg-if", "js-sys", @@ -2713,9 +2871,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.83" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2723,28 +2881,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.83" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 1.0.103", + "syn 2.0.65", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.83" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "wasm-streams" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4609d447824375f43e1ffbc051b50ad8f4b3ae8219680c94452ea05eb240ac7" +checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" dependencies = [ "futures-util", "js-sys", @@ -2755,23 +2913,14 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.60" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", ] -[[package]] -name = "wepoll-ffi" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb" -dependencies = [ - "cc", -] - [[package]] name = "winapi" version = "0.3.9" @@ -2788,15 +2937,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -2804,34 +2944,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows" -version = "0.48.0" +name = "windows-core" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.48.0", -] - -[[package]] -name = "windows-sys" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" -dependencies = [ - "windows_aarch64_msvc 0.36.1", - "windows_i686_gnu 0.36.1", - "windows_i686_msvc 0.36.1", - "windows_x86_64_gnu 0.36.1", - "windows_x86_64_msvc 0.36.1", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", + "windows-targets 0.52.5", ] [[package]] @@ -2840,158 +2958,144 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.0", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.5", ] [[package]] name = "windows-targets" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] name = "windows-targets" -version = "0.48.0" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ - "windows_aarch64_gnullvm 0.48.0", - "windows_aarch64_msvc 0.48.0", - "windows_i686_gnu 0.48.0", - "windows_i686_msvc 0.48.0", - "windows_x86_64_gnu 0.48.0", - "windows_x86_64_gnullvm 0.48.0", - "windows_x86_64_msvc 0.48.0", + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" [[package]] name = "windows_aarch64_msvc" -version = "0.36.1" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.42.2" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" [[package]] name = "windows_i686_gnu" -version = "0.36.1" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.42.2" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" [[package]] -name = "windows_i686_gnu" -version = "0.48.0" +name = "windows_i686_gnullvm" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" [[package]] name = "windows_i686_msvc" -version = "0.36.1" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.42.2" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" [[package]] name = "windows_x86_64_gnu" -version = "0.36.1" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.42.2" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.0" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" [[package]] name = "windows_x86_64_msvc" -version = "0.36.1" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.42.2" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" -version = "0.4.1" +version = "0.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" dependencies = [ "memchr", ] @@ -3017,20 +3121,22 @@ dependencies = [ [[package]] name = "xattr" -version = "0.2.3" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc" +checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" dependencies = [ "libc", + "linux-raw-sys 0.4.14", + "rustix 0.38.34", ] [[package]] name = "xdg-home" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2769203cd13a0c6015d515be729c526d041e9cf2c0cc478d57faee85f40c6dcd" +checksum = "21e5a325c3cb8398ad6cf859c1135b25dd29e186679cf2da7581d9679f63b38e" dependencies = [ - "nix", + "libc", "winapi", ] @@ -3046,9 +3152,9 @@ dependencies = [ [[package]] name = "zbus" -version = "3.13.1" +version = "3.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c3d77c9966c28321f1907f0b6c5a5561189d1f7311eea6d94180c6be9daab29" +checksum = "675d170b632a6ad49804c8cf2105d7c31eddd3312555cffd4b740e08e97c25e6" dependencies = [ "async-broadcast", "async-process", @@ -3057,7 +3163,7 @@ dependencies = [ "byteorder", "derivative", "enumflags2", - "event-listener", + "event-listener 2.5.3", "futures-core", "futures-sink", "futures-util", @@ -3082,24 +3188,23 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "3.13.1" +version = "3.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6e341d12edaff644e539ccbbf7f161601294c9a84ed3d7e015da33155b435af" +checksum = "7131497b0f887e8061b430c530240063d33bf9455fa34438f388a245da69e0a5" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "regex", - "syn 1.0.103", - "winnow", + "syn 1.0.109", "zvariant_utils", ] [[package]] name = "zbus_names" -version = "2.5.1" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82441e6033be0a741157a72951a3e4957d519698f3a824439cc131c5ba77ac2a" +checksum = "437d738d3750bed6ca9b8d423ccc7a8eb284f6b1d6d4e225a0e4e6258d864c8d" dependencies = [ "serde", "static_assertions", @@ -3108,9 +3213,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.3.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" [[package]] name = "zip" @@ -3127,9 +3232,9 @@ dependencies = [ [[package]] name = "zvariant" -version = "3.14.0" +version = "3.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622cc473f10cef1b0d73b7b34a266be30ebdcfaea40ec297dd8cbda088f9f93c" +checksum = "4eef2be88ba09b358d3b58aca6e41cd853631d44787f319a1383ca83424fb2db" dependencies = [ "byteorder", "enumflags2", @@ -3141,14 +3246,14 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "3.14.0" +version = "3.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d9c1b57352c25b778257c661f3c4744b7cefb7fc09dd46909a153cce7773da2" +checksum = "37c24dc0bed72f5f90d1f8bb5b07228cbf63b3c6e9f82d82559d4bae666e7ed9" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 1.0.103", + "syn 1.0.109", "zvariant_utils", ] @@ -3160,5 +3265,5 @@ checksum = "7234f0d811589db492d16893e3f21e8e2fd282e6d01b0cddee310322062cc200" dependencies = [ "proc-macro2", "quote", - "syn 1.0.103", + "syn 1.0.109", ] diff --git a/cli/Cargo.toml b/cli/Cargo.toml index db058cd9f7c..b820ffcc50f 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -35,12 +35,12 @@ chrono = { version = "0.4.26", features = ["serde", "std", "clock"], default-fea gethostname = "0.4.3" libc = "0.2.144" tunnels = { git = "https://github.com/microsoft/dev-tunnels", rev = "8cae9b2a24c65c6c1958f5a0e77d72b23b5c6c30", default-features = false, features = ["connections"] } -keyring = { version = "2.0.3", default-features = false, features = ["linux-secret-service-rt-tokio-crypto-openssl"] } +keyring = { version = "2.0.3", default-features = false, features = ["linux-secret-service-rt-tokio-crypto-openssl", "platform-windows", "platform-macos", "linux-keyutils"] } dialoguer = "0.10.4" hyper = { version = "0.14.26", features = ["server", "http1", "runtime"] } indicatif = "0.17.4" tempfile = "3.5.0" -clap_lex = "0.5.0" +clap_lex = "0.7.0" url = "2.3.1" async-trait = "0.1.68" log = "0.4.18" diff --git a/cli/src/commands/args.rs b/cli/src/commands/args.rs index 8a943826615..79c4d3767a1 100644 --- a/cli/src/commands/args.rs +++ b/cli/src/commands/args.rs @@ -789,11 +789,11 @@ pub enum TunnelUserSubCommands { #[derive(Args, Debug, Clone)] pub struct LoginArgs { /// An access token to store for authentication. - #[clap(long, requires = "provider")] + #[clap(long, requires = "provider", env = "VSCODE_CLI_ACCESS_TOKEN")] pub access_token: Option, /// An access token to store for authentication. - #[clap(long, requires = "access_token")] + #[clap(long, requires = "access_token", env = "VSCODE_CLI_REFRESH_TOKEN")] pub refresh_token: Option, /// The auth provider to use. If not provided, a prompt will be shown. diff --git a/cli/src/rpc.rs b/cli/src/rpc.rs index 0972ad05475..d48d777a5dc 100644 --- a/cli/src/rpc.rs +++ b/cli/src/rpc.rs @@ -634,6 +634,7 @@ const METHOD_STREAMS_STARTED: &str = "streams_started"; const METHOD_STREAM_DATA: &str = "stream_data"; const METHOD_STREAM_ENDED: &str = "stream_ended"; +#[allow(dead_code)] // false positive trait AssertIsSync: Sync {} impl AssertIsSync for RpcDispatcher {} diff --git a/extensions/tunnel-forwarding/src/extension.ts b/extensions/tunnel-forwarding/src/extension.ts index 09e0d447cdb..3dc88224aaa 100644 --- a/extensions/tunnel-forwarding/src/extension.ts +++ b/extensions/tunnel-forwarding/src/extension.ts @@ -260,12 +260,10 @@ class TunnelProvider implements vscode.TunnelProvider { 'forward-internal', '--provider', 'github', - '--access-token', - session.accessToken, ]; this.logger.log('info', '[forwarding] starting CLI'); - const child = spawn(cliPath, args, { stdio: 'pipe', env: { ...process.env, NO_COLOR: '1' } }); + const child = spawn(cliPath, args, { stdio: 'pipe', env: { ...process.env, NO_COLOR: '1', VSCODE_CLI_ACCESS_TOKEN: session.accessToken } }); this.state = { state: State.Starting, process: child }; const progressP = new DeferredPromise(); diff --git a/src/vs/platform/remoteTunnel/node/remoteTunnelService.ts b/src/vs/platform/remoteTunnel/node/remoteTunnelService.ts index 30dc15f3db5..d0d433a33e6 100644 --- a/src/vs/platform/remoteTunnel/node/remoteTunnelService.ts +++ b/src/vs/platform/remoteTunnel/node/remoteTunnelService.ts @@ -306,7 +306,7 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ a = a.replaceAll(token, '*'.repeat(4)); onOutput(a, isErr); }; - const loginProcess = this.runCodeTunnelCommand('login', ['user', 'login', '--provider', session.providerId, '--access-token', token, '--log', LogLevelToString(this._logger.getLevel())], onLoginOutput); + const loginProcess = this.runCodeTunnelCommand('login', ['user', 'login', '--provider', session.providerId, '--log', LogLevelToString(this._logger.getLevel())], onLoginOutput, { VSCODE_CLI_ACCESS_TOKEN: token }); this._tunnelProcess = loginProcess; try { await loginProcess; @@ -408,7 +408,7 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ }); } - private runCodeTunnelCommand(logLabel: string, commandArgs: string[], onOutput: (message: string, isError: boolean) => void = this.defaultOnOutput): CancelablePromise { + private runCodeTunnelCommand(logLabel: string, commandArgs: string[], onOutput: (message: string, isError: boolean) => void = this.defaultOnOutput, env?: Record): CancelablePromise { return createCancelablePromise(token => { return new Promise((resolve, reject) => { if (token.isCancellationRequested) { @@ -426,12 +426,12 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ if (!this.environmentService.isBuilt) { onOutput('Building tunnel CLI from sources and run\n', false); onOutput(`${logLabel} Spawning: cargo run -- tunnel ${commandArgs.join(' ')}\n`, false); - tunnelProcess = spawn('cargo', ['run', '--', 'tunnel', ...commandArgs], { cwd: join(this.environmentService.appRoot, 'cli'), stdio }); + tunnelProcess = spawn('cargo', ['run', '--', 'tunnel', ...commandArgs], { cwd: join(this.environmentService.appRoot, 'cli'), stdio, env: { ...process.env, RUST_BACKTRACE: '1', ...env } }); } else { onOutput('Running tunnel CLI\n', false); const tunnelCommand = this.getTunnelCommandLocation(); onOutput(`${logLabel} Spawning: ${tunnelCommand} tunnel ${commandArgs.join(' ')}\n`, false); - tunnelProcess = spawn(tunnelCommand, ['tunnel', ...commandArgs], { cwd: homedir(), stdio }); + tunnelProcess = spawn(tunnelCommand, ['tunnel', ...commandArgs], { cwd: homedir(), stdio, env: { ...process.env, ...env } }); } tunnelProcess.stdout!.pipe(new StreamSplitter('\n')).on('data', data => { From ae0c9285a1eaf7c3369329e5ee0451362a77b4cf Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Mon, 20 May 2024 09:53:35 -0700 Subject: [PATCH 273/357] if no selected items, return to accessibility help dialog (#213075) --- .../contrib/accessibility/browser/accessibleView.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts index 25f23161a5a..02af9162841 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts @@ -364,6 +364,7 @@ export class AccessibleView extends Disposable { configureKeybindings(): void { const items = this._currentProvider?.options?.configureKeybindingItems; + const provider = this._currentProvider; if (!items) { return; } @@ -380,7 +381,12 @@ export class AccessibleView extends Disposable { } quickPick.dispose(); }); - quickPick.onDidHide(() => quickPick.dispose()); + quickPick.onDidHide(() => { + if (!quickPick.selectedItems.length && provider) { + this.show(provider); + } + quickPick.dispose(); + }); } private _convertTokensToSymbols(tokens: marked.TokensList, symbols: IAccessibleViewSymbol[]): void { From b973558e5846ecd26e0263207d4ba2cd79c9518b Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Mon, 20 May 2024 10:06:20 -0700 Subject: [PATCH 274/357] provider keyboard users with hover actions (#213077) --- src/vs/workbench/contrib/chat/browser/chatListRenderer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 76b491f5848..a3417d2154f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -319,7 +319,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer Date: Mon, 20 May 2024 10:09:02 -0700 Subject: [PATCH 275/357] =?UTF-8?q?feat:=20allow=20=E2=9E=A1=EF=B8=8F=20to?= =?UTF-8?q?=20add=20chat=20context=20in=20background=20(#213078)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../quickinput/browser/pickerQuickAccess.ts | 8 +++++ .../platform/quickinput/common/quickAccess.ts | 10 ++++-- .../browser/actions/chatContextActions.ts | 36 +++++++++++-------- 3 files changed, 37 insertions(+), 17 deletions(-) diff --git a/src/vs/platform/quickinput/browser/pickerQuickAccess.ts b/src/vs/platform/quickinput/browser/pickerQuickAccess.ts index 86160c9e26a..0613c557abc 100644 --- a/src/vs/platform/quickinput/browser/pickerQuickAccess.ts +++ b/src/vs/platform/quickinput/browser/pickerQuickAccess.ts @@ -326,6 +326,14 @@ export abstract class PickerQuickAccessProvider { + if (runOptions?.handleAccept) { + if (!event.inBackground) { + picker.hide(); // hide picker unless we accept in background + } + runOptions.handleAccept?.(picker.activeItems[0]); + return; + } + const [item] = picker.selectedItems; if (typeof item?.accept === 'function') { if (!event.inBackground) { diff --git a/src/vs/platform/quickinput/common/quickAccess.ts b/src/vs/platform/quickinput/common/quickAccess.ts index 23c80d0246d..4e6e3e4e671 100644 --- a/src/vs/platform/quickinput/common/quickAccess.ts +++ b/src/vs/platform/quickinput/common/quickAccess.ts @@ -16,6 +16,12 @@ import { Registry } from 'vs/platform/registry/common/platform'; export interface IQuickAccessProviderRunOptions { readonly from?: string; readonly placeholder?: string; + /** + * A handler to invoke when an item is accepted for + * this particular showing of the quick access. + * @param item The item that was accepted. + */ + readonly handleAccept?: (item: IQuickPickItem) => void; } /** @@ -54,7 +60,7 @@ export interface IQuickAccessOptions { /** * Provider specific options for this particular showing of the * quick access. - */ + */ readonly providerOptions?: IQuickAccessProviderRunOptions; /** @@ -65,7 +71,7 @@ export interface IQuickAccessOptions { /** * A placeholder to use for this particular showing of the quick access. - */ + */ readonly placeholder?: string; } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index a6516a796de..a60487b7169 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -13,7 +13,7 @@ import { localize, localize2 } from 'vs/nls'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { AnythingQuickAccessProviderRunOptions } from 'vs/platform/quickinput/common/quickAccess'; -import { IQuickInputService, QuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { IQuickInputService, IQuickPickItem, QuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; import { IChatWidget, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { SelectAndInsertFileAction } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables'; @@ -56,10 +56,26 @@ class AttachContextAction extends Action2 { }); } + private _attachContext(widget: IChatWidget, ...picks: IQuickPickItem[]) { + widget?.attachContext(...picks.map((p) => ({ + fullName: p.label, + icon: 'icon' in p && ThemeIcon.isThemeIcon(p.icon) ? p.icon : undefined, + name: 'name' in p && typeof p.name === 'string' ? p.name : p.label, + value: 'resource' in p && URI.isUri(p.resource) ? p.resource : undefined, + id: 'id' in p && typeof p.id === 'string' ? p.id : + 'resource' in p && URI.isUri(p.resource) ? `${SelectAndInsertFileAction.Name}:${p.resource.toString()}` : '' + }))); + } + override async run(accessor: ServicesAccessor, ...args: any[]): Promise { const quickInputService = accessor.get(IQuickInputService); const chatVariablesService = accessor.get(IChatVariablesService); const widgetService = accessor.get(IChatWidgetService); + const context: { widget?: IChatWidget } | undefined = args[0]; + const widget = context?.widget ?? widgetService.lastFocusedWidget; + if (!widget) { + return; + } const quickPickItems: (QuickPickItem & { name?: string; icon?: ThemeIcon })[] = []; for (const variable of chatVariablesService.getVariables()) { @@ -72,10 +88,13 @@ class AttachContextAction extends Action2 { quickPickItems.push(SelectAndInsertFileAction.Item, { type: 'separator' }); } - const picks = await quickInputService.quickAccess.pick('', { + quickInputService.quickAccess.show('', { enabledProviderPrefixes: [AnythingQuickAccessProvider.PREFIX], placeholder: localize('chatContext.attach.placeholder', 'Search attachments'), providerOptions: { + handleAccept: (item: IQuickPickItem) => { + this._attachContext(widget, item); + }, additionPicks: quickPickItems, includeSymbols: false, filter: (item) => { @@ -87,18 +106,5 @@ class AttachContextAction extends Action2 { } }); - if (picks?.length) { - const context: { widget?: IChatWidget } | undefined = args[0]; - - const widget = context?.widget ?? widgetService.lastFocusedWidget; - widget?.attachContext(...picks.map((p) => ({ - fullName: p.label, - icon: 'icon' in p && ThemeIcon.isThemeIcon(p.icon) ? p.icon : undefined, - name: 'name' in p && typeof p.name === 'string' ? p.name : p.label, - value: 'resource' in p && URI.isUri(p.resource) ? p.resource : undefined, - id: 'id' in p && typeof p.id === 'string' ? p.id : - 'resource' in p && URI.isUri(p.resource) ? `${SelectAndInsertFileAction.Name}:${p.resource.toString()}` : '' - }))); - } } } From cb3c3cb12548c4e7646e22aa4719129a50425d71 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Mon, 20 May 2024 10:16:22 -0700 Subject: [PATCH 276/357] eng: fix prelaunch task on recent node versions Closes #212888 --- build/lib/preLaunch.js | 2 +- build/lib/preLaunch.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build/lib/preLaunch.js b/build/lib/preLaunch.js index efcb3220084..1bfe7f573f6 100644 --- a/build/lib/preLaunch.js +++ b/build/lib/preLaunch.js @@ -12,7 +12,7 @@ const yarn = process.platform === 'win32' ? 'yarn.cmd' : 'yarn'; const rootDir = path.resolve(__dirname, '..', '..'); function runProcess(command, args = []) { return new Promise((resolve, reject) => { - const child = (0, child_process_1.spawn)(command, args, { cwd: rootDir, stdio: 'inherit', env: process.env }); + const child = (0, child_process_1.spawn)(command, args, { cwd: rootDir, stdio: 'inherit', env: process.env, shell: process.platform === 'win32' }); child.on('exit', err => !err ? resolve() : process.exit(err ?? 1)); child.on('error', reject); }); diff --git a/build/lib/preLaunch.ts b/build/lib/preLaunch.ts index 3d3f513b591..d6776e62798 100644 --- a/build/lib/preLaunch.ts +++ b/build/lib/preLaunch.ts @@ -14,7 +14,7 @@ const rootDir = path.resolve(__dirname, '..', '..'); function runProcess(command: string, args: ReadonlyArray = []) { return new Promise((resolve, reject) => { - const child = spawn(command, args, { cwd: rootDir, stdio: 'inherit', env: process.env }); + const child = spawn(command, args, { cwd: rootDir, stdio: 'inherit', env: process.env, shell: process.platform === 'win32' }); child.on('exit', err => !err ? resolve() : process.exit(err ?? 1)); child.on('error', reject); }); From f5162a75337dca7671d244ba0691e68268baf294 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Mon, 20 May 2024 10:18:45 -0700 Subject: [PATCH 277/357] fix: limit width of chat attachment widgets --- src/vs/workbench/contrib/chat/browser/media/chat.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index f075098745a..5c37a078b78 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -515,6 +515,7 @@ border: 1px solid var(--vscode-chat-requestBorder, var(--vscode-input-background, transparent)); border-radius: 4px; height: 18px; + max-width: 100%; } .interactive-session-followups { From 7828baaa8e29dcc0f8a7cb32d4bf1606b42544ff Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Mon, 20 May 2024 10:26:43 -0700 Subject: [PATCH 278/357] eng: fix off-by-1 error in selfhost test error messages (#213081) Fixes #212808 --- .../vscode-selfhost-test-provider/src/testOutputScanner.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts index 1a1c21fd2c8..74013dcd561 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts @@ -204,7 +204,7 @@ export async function scanTestOutput( return; } - const logLocation = store.getSourceLocation(match[2], Number(match[3])); + const logLocation = store.getSourceLocation(match[2], Number(match[3]) - 1); const logContents = replaceAllLocations(store, match[1]); const test = currentTest; @@ -459,7 +459,8 @@ export class SourceMapStore { }; } - async getSourceLocation(fileUri: string, line: number, col = 1) { + /** Gets an original location from a base 0 line and column */ + async getSourceLocation(fileUri: string, line: number, col = 0) { return this.getSourceLocationMapper(fileUri).then(m => m(line, col)); } @@ -599,5 +600,5 @@ async function tryDeriveStackLocation( async function deriveSourceLocation(store: SourceMapStore, parts: RegExpMatchArray) { const [, fileUri, line, col] = parts; - return store.getSourceLocation(fileUri, Number(line), Number(col)); + return store.getSourceLocation(fileUri, Number(line) - 1, Number(col)); } From d68eddc5cc0e277df96a1748742c7f6b7f183328 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Mon, 20 May 2024 11:20:56 -0700 Subject: [PATCH 279/357] Fix chat welcome message tooltip (#213084) --- src/vs/workbench/contrib/chat/browser/chatFollowups.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatFollowups.ts b/src/vs/workbench/contrib/chat/browser/chatFollowups.ts index f0f4e3e0afa..eb5f7c99cdf 100644 --- a/src/vs/workbench/contrib/chat/browser/chatFollowups.ts +++ b/src/vs/workbench/contrib/chat/browser/chatFollowups.ts @@ -33,7 +33,6 @@ export class ChatFollowups extend } private renderFollowup(container: HTMLElement, followup: T): void { - if (followup.kind === 'command' && followup.when && !this.contextService.contextMatchesRules(ContextKeyExpr.deserialize(followup.when))) { return; } @@ -60,9 +59,9 @@ export class ChatFollowups extend const baseTitle = followup.kind === 'reply' ? (followup.title || followup.message) : followup.title; - - const tooltip = tooltipPrefix + - ('tooltip' in followup && followup.tooltip || baseTitle); + const message = followup.kind === 'reply' ? followup.message : followup.title; + const tooltip = (tooltipPrefix + + ('tooltip' in followup && followup.tooltip || message)).trim(); const button = this._register(new Button(container, { ...this.options, title: tooltip })); if (followup.kind === 'reply') { button.element.classList.add('interactive-followup-reply'); From 8372399982b902a1432adc886e8b024ae22ebc23 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Mon, 20 May 2024 14:23:44 -0700 Subject: [PATCH 280/357] fix a few accessible view bugs (#213097) --- .../platform/accessibility/browser/accessibleViewRegistry.ts | 1 + .../workbench/contrib/accessibility/browser/accessibleView.ts | 2 +- .../contrib/accessibility/browser/accessibleViewActions.ts | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/vs/platform/accessibility/browser/accessibleViewRegistry.ts b/src/vs/platform/accessibility/browser/accessibleViewRegistry.ts index 9390da5a534..13679e64781 100644 --- a/src/vs/platform/accessibility/browser/accessibleViewRegistry.ts +++ b/src/vs/platform/accessibility/browser/accessibleViewRegistry.ts @@ -7,6 +7,7 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { AccessibleViewType, AdvancedContentProvider, ExtensionContentProvider } from 'vs/platform/accessibility/browser/accessibleView'; import { ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { alert } from 'vs/base/browser/ui/aria/aria'; export interface IAccessibleViewImplentation { type: AccessibleViewType; diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts index 02af9162841..c4f63c9dccb 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts @@ -331,7 +331,7 @@ export class AccessibleView extends Disposable { inBlock = true; startLine = i + 1; languageId = line.substring(3).trim(); - } else if (inBlock && line.startsWith('```')) { + } else if (inBlock && line.endsWith('```')) { inBlock = false; const endLine = i; const code = lines.slice(startLine, endLine).join('\n'); diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts index 8d9bd29623d..3a437fcbc14 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts @@ -66,7 +66,7 @@ class AccessibleViewNextCodeBlockAction extends Action2 { menu: { ...accessibleViewMenu, - when: ContextKeyExpr.and(accessibleViewIsShown, accessibleViewSupportsNavigation), + when: ContextKeyExpr.and(accessibleViewIsShown, accessibleViewContainsCodeBlocks), }, title: localize('editor.action.accessibleViewNextCodeBlock', "Accessible View: Next Code Block") }); @@ -91,7 +91,7 @@ class AccessibleViewPreviousCodeBlockAction extends Action2 { icon: Codicon.arrowLeft, menu: { ...accessibleViewMenu, - when: ContextKeyExpr.and(accessibleViewIsShown, accessibleViewSupportsNavigation), + when: ContextKeyExpr.and(accessibleViewIsShown, accessibleViewContainsCodeBlocks), }, title: localize('editor.action.accessibleViewPreviousCodeBlock', "Accessible View: Previous Code Block") }); From 32cf35e7e6f42cbea856b1d23a08fed505f692b9 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Mon, 20 May 2024 14:46:12 -0700 Subject: [PATCH 281/357] Respect isSlow for chat attachments (#213105) --- .../contrib/chat/browser/actions/chatContextActions.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index a60487b7169..420560ee01d 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -19,6 +19,7 @@ import { IChatWidget, IChatWidgetService } from 'vs/workbench/contrib/chat/brows import { SelectAndInsertFileAction } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables'; import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; import { CONTEXT_CHAT_LOCATION, CONTEXT_IN_CHAT_INPUT } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { ChatRequestAgentPart } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import { AnythingQuickAccessProvider } from 'vs/workbench/contrib/search/browser/anythingQuickAccess'; @@ -77,9 +78,11 @@ class AttachContextAction extends Action2 { return; } + const usedAgent = widget.parsedInput.parts.find(p => p instanceof ChatRequestAgentPart); + const slowSupported = usedAgent ? usedAgent.agent.metadata.supportsSlowVariables : true; const quickPickItems: (QuickPickItem & { name?: string; icon?: ThemeIcon })[] = []; for (const variable of chatVariablesService.getVariables()) { - if (variable.fullName) { + if (variable.fullName && (!variable.isSlow || slowSupported)) { quickPickItems.push({ label: `${variable.icon ? `$(${variable.icon.id}) ` : ''}${variable.fullName}`, name: variable.name, id: variable.id, icon: variable.icon }); } } From b5846dbfbbabd874be6d50da1669b99a778f1ef9 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Mon, 20 May 2024 15:18:46 -0700 Subject: [PATCH 282/357] strip markdown from chat accessible view, maintain code block nav functionality (#213096) --- src/vs/base/browser/markdownRenderer.ts | 15 ++++++++++++--- .../chat/browser/chatResponseAccessibleView.ts | 5 +++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts index ea0dde2b599..d5d424d9134 100644 --- a/src/vs/base/browser/markdownRenderer.ts +++ b/src/vs/base/browser/markdownRenderer.ts @@ -492,15 +492,16 @@ export function renderStringAsPlaintext(string: IMarkdownString | string) { /** * Strips all markdown from `markdown`. For example `# Header` would be output as `Header`. + * provide @param withCodeBlocks to retain code blocks */ -export function renderMarkdownAsPlaintext(markdown: IMarkdownString) { +export function renderMarkdownAsPlaintext(markdown: IMarkdownString, withCodeBlocks?: boolean) { // values that are too long will freeze the UI let value = markdown.value ?? ''; if (value.length > 100_000) { value = `${value.substr(0, 100_000)}…`; } - const html = marked.parse(value, { renderer: plainTextRenderer.value }).replace(/&(#\d+|[a-zA-Z]+);/g, m => unescapeInfo.get(m) ?? m); + const html = marked.parse(value, { renderer: withCodeBlocks ? plainTextWithCodeBlocksRenderer.value : plainTextRenderer.value }).replace(/&(#\d+|[a-zA-Z]+);/g, m => unescapeInfo.get(m) ?? m); return sanitizeRenderedMarkdown({ isTrusted: false }, html).toString(); } @@ -514,7 +515,7 @@ const unescapeInfo = new Map([ ['>', '>'], ]); -const plainTextRenderer = new Lazy(() => { +function createRenderer(): marked.Renderer { const renderer = new marked.Renderer(); renderer.code = (code: string): string => { @@ -578,6 +579,14 @@ const plainTextRenderer = new Lazy(() => { return text; }; return renderer; +} +const plainTextRenderer = new Lazy((withCodeBlocks?: boolean) => createRenderer()); +const plainTextWithCodeBlocksRenderer = new Lazy(() => { + const renderer = createRenderer(); + renderer.code = (code: string): string => { + return '\n' + '```' + code + '```' + '\n'; + }; + return renderer; }); function mergeRawTokenText(tokens: marked.Token[]): string { diff --git a/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts b/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts index 2babe2b5131..d97ecba4f42 100644 --- a/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts +++ b/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { renderMarkdownAsPlaintext } from 'vs/base/browser/markdownRenderer'; +import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { AccessibleViewProviderId, AccessibleViewType } from 'vs/platform/accessibility/browser/accessibleView'; import { alertAccessibleViewFocusChange, IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; @@ -71,7 +72,7 @@ export class ChatResponseAccessibleView implements IAccessibleViewImplentation { return { id: AccessibleViewProviderId.Chat, verbositySettingKey: AccessibilityVerbositySettingId.Chat, - provideContent(): string { return responseContent!; }, + provideContent(): string { return renderMarkdownAsPlaintext(new MarkdownString(responseContent), true); }, onClose() { verifiedWidget.reveal(focusedItem); if (chatInputFocused) { From 4b45180a16dda25909b0a4ba62b6346a30b13bee Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Mon, 20 May 2024 16:14:23 -0700 Subject: [PATCH 283/357] resolve accessibility help dialog kbs in `accessibleViewService` (#213107) --- src/vs/editor/common/standaloneStrings.ts | 15 ++---- .../accessibility/browser/accessibleView.ts | 13 ++++- .../accessibleViewKeybindingResolver.ts | 40 ++++++++++++++ .../browser/editorAccessibilityHelp.ts | 23 ++------ .../extensionAccesibilityHelp.contribution.ts | 47 +++------------- .../browser/actions/chatAccessibilityHelp.ts | 42 +++++---------- .../comments/browser/commentsAccessibility.ts | 47 +++------------- .../browser/notebookAccessibilityHelp.ts | 53 +++++-------------- 8 files changed, 102 insertions(+), 178 deletions(-) create mode 100644 src/vs/workbench/contrib/accessibility/browser/accessibleViewKeybindingResolver.ts diff --git a/src/vs/editor/common/standaloneStrings.ts b/src/vs/editor/common/standaloneStrings.ts index 1bcfecfeb97..81ffaa07895 100644 --- a/src/vs/editor/common/standaloneStrings.ts +++ b/src/vs/editor/common/standaloneStrings.ts @@ -18,19 +18,14 @@ export namespace AccessibilityHelpNLS { export const auto_off = nls.localize("auto_off", "The application is configured to never be optimized for usage with a Screen Reader."); export const screenReaderModeEnabled = nls.localize("screenReaderModeEnabled", "Screen Reader Optimized Mode enabled."); export const screenReaderModeDisabled = nls.localize("screenReaderModeDisabled", "Screen Reader Optimized Mode disabled."); - export const tabFocusModeOnMsg = nls.localize("tabFocusModeOnMsg", "Pressing Tab in the current editor will move focus to the next focusable element. Toggle this behavior {0}."); - export const tabFocusModeOnMsgNoKb = nls.localize("tabFocusModeOnMsgNoKb", "Pressing Tab in the current editor will move focus to the next focusable element. The command {0} is currently not triggerable by a keybinding."); - export const stickScrollKb = nls.localize("stickScrollKb", "Focus Sticky Scroll ({0}) to focus the currently nested scopes."); - export const stickScrollNoKb = nls.localize("stickScrollNoKb", "Focus Sticky Scroll to focus the currently nested scopes. It is currently not triggerable by a keybinding."); - export const tabFocusModeOffMsg = nls.localize("tabFocusModeOffMsg", "Pressing Tab in the current editor will insert the tab character. Toggle this behavior {0}."); - export const tabFocusModeOffMsgNoKb = nls.localize("tabFocusModeOffMsgNoKb", "Pressing Tab in the current editor will insert the tab character. The command {0} is currently not triggerable by a keybinding."); + export const tabFocusModeOnMsg = nls.localize("tabFocusModeOnMsg", "Pressing Tab in the current editor will move focus to the next focusable element. Toggle this behavior"); + export const tabFocusModeOffMsg = nls.localize("tabFocusModeOffMsg", "Pressing Tab in the current editor will insert the tab character. Toggle this behavior"); + export const stickScroll = nls.localize("stickScrollKb", "Focus Sticky Scroll to focus the currently nested scopes."); export const showAccessibilityHelpAction = nls.localize("showAccessibilityHelpAction", "Show Accessibility Help"); export const listSignalSounds = nls.localize("listSignalSoundsCommand", "Run the command: List Signal Sounds for an overview of all sounds and their current status."); export const listAlerts = nls.localize("listAnnouncementsCommand", "Run the command: List Signal Announcements for an overview of announcements and their current status."); - export const quickChat = nls.localize("quickChatCommand", "Toggle quick chat ({0}) to open or close a chat session."); - export const quickChatNoKb = nls.localize("quickChatCommandNoKb", "Toggle quick chat is not currently triggerable by a keybinding."); - export const startInlineChat = nls.localize("startInlineChatCommand", "Start inline chat ({0}) to create an in editor chat session."); - export const startInlineChatNoKb = nls.localize("startInlineChatCommandNoKb", "The command: Start inline chat is not currentlyt riggerable by a keybinding."); + export const quickChat = nls.localize("quickChatCommand", "Toggle quick chat to open or close a chat session.",); + export const startInlineChat = nls.localize("startInlineChatCommand", "Start inline chat to create an in editor chat session."); } export namespace InspectTokensNLS { diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts index c4f63c9dccb..3cad6386092 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts @@ -41,6 +41,7 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IQuickInputService, IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { AccessibilityVerbositySettingId, AccessibilityWorkbenchSettingId, accessibilityHelpIsShown, accessibleViewContainsCodeBlocks, accessibleViewCurrentProviderId, accessibleViewGoToSymbolSupported, accessibleViewInCodeBlock, accessibleViewIsShown, accessibleViewOnLastLine, accessibleViewSupportsNavigation, accessibleViewVerbosityEnabled } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { resolveContentAndKeybindingItems } from 'vs/workbench/contrib/accessibility/browser/accessibleViewKeybindingResolver'; import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; import { IChatCodeBlockContextProviderService } from 'vs/workbench/contrib/chat/browser/chat'; import { ICodeBlockActionContext } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; @@ -491,7 +492,17 @@ export class AccessibleView extends Disposable { } } const exitThisDialogHint = verbose && !provider.options.position ? localize('exit', '\n\nExit this dialog (Escape).') : ''; - const newContent = message + provider.provideContent() + readMoreLink + disableHelpHint + exitThisDialogHint; + let content = provider.provideContent(); + if (provider.options.type === AccessibleViewType.Help) { + const resolvedContent = resolveContentAndKeybindingItems(this._keybindingService, content); + if (resolvedContent) { + content = resolvedContent.content.value; + if (resolvedContent.configureKeybindingItems) { + provider.options.configureKeybindingItems = resolvedContent.configureKeybindingItems; + } + } + } + const newContent = message + content + readMoreLink + disableHelpHint + exitThisDialogHint; this.calculateCodeBlocks(newContent); this._currentContent = newContent; this._updateContextKeys(provider, true); diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleViewKeybindingResolver.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleViewKeybindingResolver.ts new file mode 100644 index 00000000000..88bfbd309d4 --- /dev/null +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleViewKeybindingResolver.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { MarkdownString } from 'vs/base/common/htmlContent'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IPickerQuickAccessItem } from 'vs/platform/quickinput/browser/pickerQuickAccess'; +import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; + +export function resolveContentAndKeybindingItems(keybindingService: IKeybindingService, value?: string): { content: MarkdownString; configureKeybindingItems: IPickerQuickAccessItem[] | undefined } | undefined { + if (!value) { + return; + } + const configureKeybindingItems: IPickerQuickAccessItem[] = []; + const matches = value.matchAll(/\.*)\>/gm); + for (const match of [...matches]) { + const commandId = match?.groups?.commandId; + let kbLabel; + if (match?.length && commandId) { + const keybinding = keybindingService.lookupKeybinding(commandId)?.getAriaLabel(); + if (!keybinding) { + const configureKb = keybindingService.lookupKeybinding(AccessibilityCommandId.AccessibilityHelpConfigureKeybindings)?.getAriaLabel(); + const keybindingToConfigureQuickPick = configureKb ? '(' + configureKb + ')' : 'by assigning a keybinding to the command Accessibility Help Configure Keybindings.'; + kbLabel = `, configure a keybinding ` + keybindingToConfigureQuickPick; + configureKeybindingItems.push({ + label: commandId, + id: commandId + }); + } else { + kbLabel = ' (' + keybinding + ')'; + } + value = value.replace(match[0], kbLabel); + } + } + const content = new MarkdownString(value); + content.isTrusted = true; + return { content, configureKeybindingItems: configureKeybindingItems.length ? configureKeybindingItems : undefined }; +} + diff --git a/src/vs/workbench/contrib/accessibility/browser/editorAccessibilityHelp.ts b/src/vs/workbench/contrib/accessibility/browser/editorAccessibilityHelp.ts index 11b80cdbacb..0e47093d20b 100644 --- a/src/vs/workbench/contrib/accessibility/browser/editorAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/accessibility/browser/editorAccessibilityHelp.ts @@ -8,16 +8,13 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { AccessibilityHelpNLS } from 'vs/editor/common/standaloneStrings'; -import { ToggleTabFocusModeAction } from 'vs/editor/contrib/toggleTabFocusMode/browser/toggleTabFocusMode'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { descriptionForCommand } from 'vs/workbench/contrib/accessibility/browser/accessibleViewContributions'; import { AccessibilityHelpAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; import { CONTEXT_CHAT_ENABLED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { CommentAccessibilityHelpNLS } from 'vs/workbench/contrib/comments/browser/commentsAccessibility'; -import { CommentCommandId } from 'vs/workbench/contrib/comments/common/commentCommandIds'; import { CommentContextKeys } from 'vs/workbench/contrib/comments/common/commentContextKeys'; import { NEW_UNTITLED_FILE_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileConstants'; import { IAccessibleViewService, IAccessibleViewContentProvider, AccessibleViewProviderId, IAccessibleViewOptions, AccessibleViewType } from 'vs/platform/accessibility/browser/accessibleView'; @@ -88,13 +85,13 @@ class EditorAccessibilityHelpProvider implements IAccessibleViewContentProvider } if (options.get(EditorOption.stickyScroll).enabled) { - content.push(descriptionForCommand('editor.action.focusStickyScroll', AccessibilityHelpNLS.stickScrollKb, AccessibilityHelpNLS.stickScrollNoKb, this._keybindingService)); + content.push(AccessibilityHelpNLS.stickScroll); } if (options.get(EditorOption.tabFocusMode)) { - content.push(descriptionForCommand(ToggleTabFocusModeAction.ID, AccessibilityHelpNLS.tabFocusModeOnMsg, AccessibilityHelpNLS.tabFocusModeOnMsgNoKb, this._keybindingService)); + content.push(AccessibilityHelpNLS.tabFocusModeOnMsg); } else { - content.push(descriptionForCommand(ToggleTabFocusModeAction.ID, AccessibilityHelpNLS.tabFocusModeOffMsg, AccessibilityHelpNLS.tabFocusModeOffMsgNoKb, this._keybindingService)); + content.push(AccessibilityHelpNLS.tabFocusModeOffMsg); } return content.join('\n\n'); } @@ -103,24 +100,14 @@ class EditorAccessibilityHelpProvider implements IAccessibleViewContentProvider export function getCommentCommandInfo(keybindingService: IKeybindingService, contextKeyService: IContextKeyService, editor: ICodeEditor): string | undefined { const editorContext = contextKeyService.getContext(editor.getDomNode()!); if (editorContext.getValue(CommentContextKeys.activeEditorHasCommentingRange.key)) { - const commentCommandInfo: string[] = []; - commentCommandInfo.push(CommentAccessibilityHelpNLS.intro); - commentCommandInfo.push(descriptionForCommand(CommentCommandId.Add, CommentAccessibilityHelpNLS.addComment, CommentAccessibilityHelpNLS.addCommentNoKb, keybindingService)); - commentCommandInfo.push(descriptionForCommand(CommentCommandId.NextThread, CommentAccessibilityHelpNLS.nextCommentThreadKb, CommentAccessibilityHelpNLS.nextCommentThreadNoKb, keybindingService)); - commentCommandInfo.push(descriptionForCommand(CommentCommandId.PreviousThread, CommentAccessibilityHelpNLS.previousCommentThreadKb, CommentAccessibilityHelpNLS.previousCommentThreadNoKb, keybindingService)); - commentCommandInfo.push(descriptionForCommand(CommentCommandId.NextRange, CommentAccessibilityHelpNLS.nextRange, CommentAccessibilityHelpNLS.nextRangeNoKb, keybindingService)); - commentCommandInfo.push(descriptionForCommand(CommentCommandId.PreviousRange, CommentAccessibilityHelpNLS.previousRange, CommentAccessibilityHelpNLS.previousRangeNoKb, keybindingService)); - return commentCommandInfo.join('\n'); + return [CommentAccessibilityHelpNLS.intro, CommentAccessibilityHelpNLS.addComment, CommentAccessibilityHelpNLS.nextCommentThread, CommentAccessibilityHelpNLS.previousCommentThread, CommentAccessibilityHelpNLS.nextRange, CommentAccessibilityHelpNLS.previousRange].join('\n'); } return; } export function getChatCommandInfo(keybindingService: IKeybindingService, contextKeyService: IContextKeyService): string | undefined { if (CONTEXT_CHAT_ENABLED.getValue(contextKeyService)) { - const commentCommandInfo: string[] = []; - commentCommandInfo.push(descriptionForCommand('workbench.action.quickchat.toggle', AccessibilityHelpNLS.quickChat, AccessibilityHelpNLS.quickChatNoKb, keybindingService)); - commentCommandInfo.push(descriptionForCommand('inlineChat.start', AccessibilityHelpNLS.startInlineChat, AccessibilityHelpNLS.startInlineChatNoKb, keybindingService)); - return commentCommandInfo.join('\n'); + return [AccessibilityHelpNLS.quickChat, AccessibilityHelpNLS.startInlineChat].join('\n'); } return; } diff --git a/src/vs/workbench/contrib/accessibility/browser/extensionAccesibilityHelp.contribution.ts b/src/vs/workbench/contrib/accessibility/browser/extensionAccesibilityHelp.contribution.ts index 2e5056bc567..46ea96cf1f0 100644 --- a/src/vs/workbench/contrib/accessibility/browser/extensionAccesibilityHelp.contribution.ts +++ b/src/vs/workbench/contrib/accessibility/browser/extensionAccesibilityHelp.contribution.ts @@ -3,17 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { MarkdownString } from 'vs/base/common/htmlContent'; import { DisposableMap, IDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { AccessibleViewType, ExtensionContentProvider } from 'vs/platform/accessibility/browser/accessibleView'; import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; -import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { IPickerQuickAccessItem } from 'vs/platform/quickinput/browser/pickerQuickAccess'; import { Registry } from 'vs/platform/registry/common/platform'; import { FocusedViewContext } from 'vs/workbench/common/contextkeys'; import { IViewsRegistry, Extensions, IViewDescriptor } from 'vs/workbench/common/views'; -import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; export class ExtensionAccessibilityHelpDialogContribution extends Disposable { @@ -42,9 +39,9 @@ export class ExtensionAccessibilityHelpDialogContribution extends Disposable { function registerAccessibilityHelpAction(keybindingService: IKeybindingService, viewDescriptor: IViewDescriptor): IDisposable { const disposableStore = new DisposableStore(); - const helpContent = resolveExtensionHelpContent(keybindingService, viewDescriptor.accessibilityHelpContent); - if (!helpContent) { - throw new Error('No help content for view'); + const content = viewDescriptor.accessibilityHelpContent?.value; + if (!content) { + throw new Error('No content provided for the accessibility help dialog'); } disposableStore.add(AccessibleViewRegistry.register({ priority: 95, @@ -55,8 +52,8 @@ function registerAccessibilityHelpAction(keybindingService: IKeybindingService, const viewsService = accessor.get(IViewsService); return new ExtensionContentProvider( viewDescriptor.id, - { type: AccessibleViewType.Help, configureKeybindingItems: helpContent.configureKeybindingItems }, - () => helpContent.value.value, + { type: AccessibleViewType.Help }, + () => content, () => viewsService.openView(viewDescriptor.id, true), ); } @@ -68,35 +65,3 @@ function registerAccessibilityHelpAction(keybindingService: IKeybindingService, })); return disposableStore; } - -function resolveExtensionHelpContent(keybindingService: IKeybindingService, content?: MarkdownString): { value: MarkdownString; configureKeybindingItems: IPickerQuickAccessItem[] | undefined } | undefined { - if (!content) { - return; - } - const configureKeybindingItems: IPickerQuickAccessItem[] = []; - let resolvedContent = typeof content === 'string' ? content : content.value; - const matches = resolvedContent.matchAll(/\.*)\>/gm); - for (const match of [...matches]) { - const commandId = match?.groups?.commandId; - let kbLabel; - if (match?.length && commandId) { - const keybinding = keybindingService.lookupKeybinding(commandId)?.getAriaLabel(); - if (!keybinding) { - const configureKb = keybindingService.lookupKeybinding(AccessibilityCommandId.AccessibilityHelpConfigureKeybindings)?.getAriaLabel(); - const keybindingToConfigureQuickPick = configureKb ? '(' + configureKb + ')' : 'by assigning a keybinding to the command accessibility.openQuickPick.'; - kbLabel = `, configure a keybinding ` + keybindingToConfigureQuickPick; - configureKeybindingItems.push({ - label: commandId, - id: commandId - }); - } else { - kbLabel = ' (' + keybinding + ')'; - } - resolvedContent = resolvedContent.replace(match[0], kbLabel); - } - } - const value = new MarkdownString(resolvedContent); - value.isTrusted = true; - return { value, configureKeybindingItems: configureKeybindingItems.length ? configureKeybindingItems : undefined }; -} - diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts index bd6d4053c81..d7ec863b045 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts @@ -4,8 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { format } from 'vs/base/common/strings'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; @@ -29,49 +27,33 @@ export class ChatAccessibilityHelp implements IAccessibleViewImplentation { } } -export function getAccessibilityHelpText(accessor: ServicesAccessor, type: 'panelChat' | 'inlineChat'): string { - const keybindingService = accessor.get(IKeybindingService); +export function getAccessibilityHelpText(type: 'panelChat' | 'inlineChat'): string { const content = []; - const openAccessibleViewKeybinding = keybindingService.lookupKeybinding('editor.action.accessibleView')?.getAriaLabel(); if (type === 'panelChat') { content.push(localize('chat.overview', 'The chat view is comprised of an input box and a request/response list. The input box is used to make requests and the list is used to display responses.')); content.push(localize('chat.requestHistory', 'In the input box, use up and down arrows to navigate your request history. Edit input and use enter or the submit button to run a new request.')); - content.push(openAccessibleViewKeybinding ? localize('chat.inspectResponse', 'In the input box, inspect the last response in the accessible view {0}', openAccessibleViewKeybinding) : localize('chat.inspectResponseNoKb', 'With the input box focused, inspect the last response in the accessible view via the Open Accessible View command, which is currently not triggerable by a keybinding.')); + content.push(localize('chat.inspectResponse', 'In the input box, inspect the last response in the accessible view')); content.push(localize('chat.followUp', 'In the input box, navigate to the suggested follow up question (Shift+Tab) and press Enter to run it.')); content.push(localize('chat.announcement', 'Chat responses will be announced as they come in. A response will indicate the number of code blocks, if any, and then the rest of the response.')); - content.push(descriptionForCommand('chat.action.focus', localize('workbench.action.chat.focus', 'To focus the chat request/response list, which can be navigated with up and down arrows, invoke the Focus Chat command ({0}).',), localize('workbench.action.chat.focusNoKb', 'To focus the chat request/response list, which can be navigated with up and down arrows, invoke The Focus Chat List command, which is currently not triggerable by a keybinding.'), keybindingService)); - content.push(descriptionForCommand('workbench.action.chat.focusInput', localize('workbench.action.chat.focusInput', 'To focus the input box for chat requests, invoke the Focus Chat Input command ({0}).'), localize('workbench.action.interactiveSession.focusInputNoKb', 'To focus the input box for chat requests, invoke the Focus Chat Input command, which is currently not triggerable by a keybinding.'), keybindingService)); - content.push(descriptionForCommand('workbench.action.chat.nextCodeBlock', localize('workbench.action.chat.nextCodeBlock', 'To focus the next code block within a response, invoke the Chat: Next Code Block command ({0}).'), localize('workbench.action.chat.nextCodeBlockNoKb', 'To focus the next code block within a response, invoke the Chat: Next Code Block command, which is currently not triggerable by a keybinding.'), keybindingService)); - content.push(descriptionForCommand('workbench.action.chat.nextFileTree', localize('workbench.action.chat.nextFileTree', 'To focus the next file tree within a response, invoke the Chat: Next File Tree command ({0}).'), localize('workbench.action.chat.nextFileTreeNoKb', 'To focus the next file tree within a response, invoke the Chat: Next File Tree command, which is currently not triggerable by a keybinding.'), keybindingService)); - content.push(descriptionForCommand('workbench.action.chat.clear', localize('workbench.action.chat.clear', 'To clear the request/response list, invoke the Chat Clear command ({0}).'), localize('workbench.action.chat.clearNoKb', 'To clear the request/response list, invoke the Chat Clear command, which is currently not triggerable by a keybinding.'), keybindingService)); + content.push(localize('workbench.action.chat.focus', 'To focus the chat request/response list, which can be navigated with up and down arrows, invoke the Focus Chat command.')); + content.push(localize('workbench.action.chat.focusInput', 'To focus the input box for chat requests, invoke the Focus Chat Input command.')); + content.push(localize('workbench.action.chat.nextCodeBlock', 'To focus the next code block within a response, invoke the Chat: Next Code Block command.')); + content.push(localize('workbench.action.chat.nextFileTree', 'To focus the next file tree within a response, invoke the Chat: Next File Tree command.')); + content.push(localize('workbench.action.chat.clear', 'To clear the request/response list, invoke the Chat Clear command.')); } else { - const startChatKeybinding = keybindingService.lookupKeybinding('inlineChat.start')?.getAriaLabel(); content.push(localize('inlineChat.overview', "Inline chat occurs within a code editor and takes into account the current selection. It is useful for making changes to the current editor. For example, fixing diagnostics, documenting or refactoring code. Keep in mind that AI generated code may be incorrect.")); - content.push(localize('inlineChat.access', "It can be activated via code actions or directly using the command: Inline Chat: Start Inline Chat ({0}).", startChatKeybinding)); - const upHistoryKeybinding = keybindingService.lookupKeybinding('inlineChat.previousFromHistory')?.getAriaLabel(); - const downHistoryKeybinding = keybindingService.lookupKeybinding('inlineChat.nextFromHistory')?.getAriaLabel(); - if (upHistoryKeybinding && downHistoryKeybinding) { - content.push(localize('inlineChat.requestHistory', 'In the input box, use {0} and {1} to navigate your request history. Edit input and use enter or the submit button to run a new request.', upHistoryKeybinding, downHistoryKeybinding)); - } - content.push(openAccessibleViewKeybinding ? localize('inlineChat.inspectResponse', 'In the input box, inspect the response in the accessible view {0}.', openAccessibleViewKeybinding) : localize('inlineChat.inspectResponseNoKb', 'With the input box focused, inspect the response in the accessible view via the Open Accessible View command, which is currently not triggerable by a keybinding.')); + content.push(localize('inlineChat.access', "It can be activated via code actions or directly using the command: Inline Chat: Start Inline Chat.")); + content.push(localize('inlineChat.requestHistory', 'In the input box, use and to navigate your request history. Edit input and use enter or the submit button to run a new request.')); + content.push(localize('inlineChat.inspectResponse', 'In the input box, inspect the response in the accessible viewview')); content.push(localize('inlineChat.contextActions', "Context menu actions may run a request prefixed with a /. Type / to discover such ready-made commands.")); content.push(localize('inlineChat.fix', "If a fix action is invoked, a response will indicate the problem with the current code. A diff editor will be rendered and can be reached by tabbing.")); - const diffReviewKeybinding = keybindingService.lookupKeybinding(AccessibleDiffViewerNext.id)?.getAriaLabel(); - content.push(diffReviewKeybinding ? localize('inlineChat.diff', "Once in the diff editor, enter review mode with ({0}). Use up and down arrows to navigate lines with the proposed changes.", diffReviewKeybinding) : localize('inlineChat.diffNoKb', "Tab again to enter the Diff editor with the changes and enter review mode with the Go to Next Difference Command. Use Up/DownArrow to navigate lines with the proposed changes.")); + content.push(localize('inlineChat.diff', "Once in the diff editor, enter review mode with. Use up and down arrows to navigate lines with the proposed changes.", AccessibleDiffViewerNext.id)); content.push(localize('inlineChat.toolbar', "Use tab to reach conditional parts like commands, status, message responses and more.")); } content.push(localize('chat.signals', "Accessibility Signals can be changed via settings with a prefix of signals.chat. By default, if a request takes more than 4 seconds, you will hear a sound indicating that progress is still occurring.")); return content.join('\n\n'); } -function descriptionForCommand(commandId: string, msg: string, noKbMsg: string, keybindingService: IKeybindingService): string { - const kb = keybindingService.lookupKeybinding(commandId); - if (kb) { - return format(msg, kb.getAriaLabel()); - } - return format(noKbMsg, commandId); -} - export function getChatAccessibilityHelpProvider(accessor: ServicesAccessor, editor: ICodeEditor | undefined, type: 'panelChat' | 'inlineChat') { const widgetService = accessor.get(IChatWidgetService); const inputEditor: ICodeEditor | undefined = type === 'panelChat' ? widgetService.lastFocusedWidget?.inputEditor : editor; @@ -86,7 +68,7 @@ export function getChatAccessibilityHelpProvider(accessor: ServicesAccessor, edi const cachedPosition = inputEditor.getPosition(); inputEditor.getSupportedActions(); - const helpText = getAccessibilityHelpText(accessor, type); + const helpText = getAccessibilityHelpText(type); return { id: type === 'panelChat' ? AccessibleViewProviderId.Chat : AccessibleViewProviderId.InlineChat, verbositySettingKey: type === 'panelChat' ? AccessibilityVerbositySettingId.Chat : AccessibilityVerbositySettingId.InlineChat, diff --git a/src/vs/workbench/contrib/comments/browser/commentsAccessibility.ts b/src/vs/workbench/contrib/comments/browser/commentsAccessibility.ts index 2f200bc0788..5ce9ac9a0ba 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsAccessibility.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsAccessibility.ts @@ -9,9 +9,6 @@ import { ctxCommentEditorFocused } from 'vs/workbench/contrib/comments/browser/s import { CommentContextKeys } from 'vs/workbench/contrib/comments/common/commentContextKeys'; import * as nls from 'vs/nls'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import * as strings from 'vs/base/common/strings'; -import { getActiveElement } from 'vs/base/browser/dom'; import { CommentCommandId } from 'vs/workbench/contrib/comments/common/commentCommandIds'; import { ToggleTabFocusModeAction } from 'vs/editor/contrib/toggleTabFocusMode/browser/toggleTabFocusMode'; import { IAccessibleViewContentProvider, AccessibleViewProviderId, IAccessibleViewOptions, AccessibleViewType } from 'vs/platform/accessibility/browser/accessibleView'; @@ -19,22 +16,15 @@ import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/a export namespace CommentAccessibilityHelpNLS { export const intro = nls.localize('intro', "The editor contains commentable range(s). Some useful commands include:"); - export const introWidget = nls.localize('introWidget', "This widget contains a text area, for composition of new comments, and actions, that can be tabbed to once tab moves focus mode has been enabled ({0})."); - export const introWidgetNoKb = nls.localize('introWidgetNoKb', "This widget contains a text area, for composition of new comments, and actions, that can be tabbed to once tab moves focus mode has been enabled with the command Toggle Tab Key Moves Focus, which is currently not triggerable via keybinding."); + export const tabFocus = nls.localize('introWidget', "This widget contains a text area, for composition of new comments, and actions, that can be tabbed to once tab moves focus mode has been enabled with the command Toggle Tab Key Moves Focus{0}", ``); export const commentCommands = nls.localize('commentCommands', "Some useful comment commands include:"); export const escape = nls.localize('escape', "- Dismiss Comment (Escape)"); - export const nextRange = nls.localize('next', "- Go to Next Commenting Range ({0})"); - export const nextRangeNoKb = nls.localize('nextNoKb', "- Go to Next Commenting Range, which is currently not triggerable via keybinding."); - export const previousRange = nls.localize('previous', "- Go to Previous Commenting Range ({0})"); - export const previousRangeNoKb = nls.localize('previousNoKb', "- Go to Previous Commenting Range, which is currently not triggerable via keybinding."); - export const nextCommentThreadKb = nls.localize('nextCommentThreadKb', "- Go to Next Comment Thread ({0})"); - export const nextCommentThreadNoKb = nls.localize('nextCommentThreadNoKb', "- Go to Next Comment Thread, which is currently not triggerable via keybinding."); - export const previousCommentThreadKb = nls.localize('previousCommentThreadKb', "- Go to Previous Comment Thread ({0})"); - export const previousCommentThreadNoKb = nls.localize('previousCommentThreadNoKb', "- Go to Previous Comment Thread, which is currently not triggerable via keybinding."); - export const addComment = nls.localize('addComment', "- Add Comment ({0})"); - export const addCommentNoKb = nls.localize('addCommentNoKb', "- Add Comment on Current Selection, which is currently not triggerable via keybinding."); - export const submitComment = nls.localize('submitComment', "- Submit Comment ({0})"); - export const submitCommentNoKb = nls.localize('submitCommentNoKb', "- Submit Comment, accessible via tabbing, as it's currently not triggerable with a keybinding."); + export const nextRange = nls.localize('next', "- Go to Next Commenting Range{0}", ``); + export const previousRange = nls.localize('previous', "- Go to Previous Commenting Range{0}", ``); + export const nextCommentThread = nls.localize('nextCommentThreadKb', "- Go to Next Comment Thread{0}", ``); + export const previousCommentThread = nls.localize('previousCommentThreadKb', "- Go to Previous Comment Thread{0}", ``); + export const addComment = nls.localize('addCommentNoKb', "- Add Comment on Current Selection{0}", ``); + export const submitComment = nls.localize('submitComment', "- Submit Comment{0}", ``); } export class CommentsAccessibilityHelpProvider implements IAccessibleViewContentProvider { @@ -42,29 +32,8 @@ export class CommentsAccessibilityHelpProvider implements IAccessibleViewContent verbositySettingKey: AccessibilityVerbositySettingId = AccessibilityVerbositySettingId.Comments; options: IAccessibleViewOptions = { type: AccessibleViewType.Help }; private _element: HTMLElement | undefined; - constructor( - @IKeybindingService private readonly _keybindingService: IKeybindingService - ) { - - } - private _descriptionForCommand(commandId: string, msg: string, noKbMsg: string): string { - const kb = this._keybindingService.lookupKeybinding(commandId); - if (kb) { - return strings.format(msg, kb.getAriaLabel()); - } - return strings.format(noKbMsg, commandId); - } provideContent(): string { - this._element = getActiveElement() as HTMLElement; - const content: string[] = []; - content.push(this._descriptionForCommand(ToggleTabFocusModeAction.ID, CommentAccessibilityHelpNLS.introWidget, CommentAccessibilityHelpNLS.introWidgetNoKb) + '\n'); - content.push(CommentAccessibilityHelpNLS.commentCommands); - content.push(CommentAccessibilityHelpNLS.escape); - content.push(this._descriptionForCommand(CommentCommandId.Add, CommentAccessibilityHelpNLS.addComment, CommentAccessibilityHelpNLS.addCommentNoKb)); - content.push(this._descriptionForCommand(CommentCommandId.Submit, CommentAccessibilityHelpNLS.submitComment, CommentAccessibilityHelpNLS.submitCommentNoKb)); - content.push(this._descriptionForCommand(CommentCommandId.NextRange, CommentAccessibilityHelpNLS.nextRange, CommentAccessibilityHelpNLS.nextRangeNoKb)); - content.push(this._descriptionForCommand(CommentCommandId.PreviousRange, CommentAccessibilityHelpNLS.previousRange, CommentAccessibilityHelpNLS.previousRangeNoKb)); - return content.join('\n'); + return [CommentAccessibilityHelpNLS.tabFocus, CommentAccessibilityHelpNLS.commentCommands, CommentAccessibilityHelpNLS.escape, CommentAccessibilityHelpNLS.addComment, CommentAccessibilityHelpNLS.submitComment, CommentAccessibilityHelpNLS.nextRange, CommentAccessibilityHelpNLS.previousRange].join('\n'); } onClose(): void { this._element?.focus(); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityHelp.ts b/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityHelp.ts index 24ceaf061fa..c06912b28f9 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityHelp.ts @@ -6,8 +6,6 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; import { NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { localize } from 'vs/nls'; -import { format } from 'vs/base/common/strings'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { AccessibleViewProviderId, AccessibleViewType } from 'vs/platform/accessibility/browser/accessibleView'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; @@ -34,46 +32,23 @@ export class NotebookAccessibilityHelp implements IAccessibleViewImplentation { -export function getAccessibilityHelpText(accessor: ServicesAccessor): string { - const keybindingService = accessor.get(IKeybindingService); - const content = []; - content.push(localize('notebook.overview', 'The notebook view is a collection of code and markdown cells. Code cells can be executed and will produce output directly below the cell.')); - content.push(descriptionForCommand('notebook.cell.edit', - localize('notebook.cell.edit', 'The Edit Cell command ({0}) will focus on the cell input.'), - localize('notebook.cell.editNoKb', 'The Edit Cell command will focus on the cell input and is currently not triggerable by a keybinding.'), keybindingService)); - content.push(descriptionForCommand('notebook.cell.quitEdit', - localize('notebook.cell.quitEdit', 'The Quit Edit command ({0}) will set focus on the cell container. The default (Escape) key may need to be pressed twice first exit the virtual cursor if active.'), - localize('notebook.cell.quitEditNoKb', 'The Quit Edit command will set focus on the cell container and is currently not triggerable by a keybinding.'), keybindingService)); - content.push(descriptionForCommand('notebook.cell.focusInOutput', - localize('notebook.cell.focusInOutput', 'The Focus Output command ({0}) will set focus in the cell\'s output.'), - localize('notebook.cell.focusInOutputNoKb', 'The Quit Edit command will set focus in the cell\'s output and is currently not triggerable by a keybinding.'), keybindingService)); - content.push(descriptionForCommand('notebook.focusNextEditor', - localize('notebook.focusNextEditor', 'The Focus Next Cell Editor command ({0}) will set focus in the next cell\'s editor.'), - localize('notebook.focusNextEditorNoKb', 'The Focus Next Cell Editor command will set focus in the next cell\'s editor and is currently not triggerable by a keybinding.'), keybindingService)); - content.push(descriptionForCommand('notebook.focusPreviousEditor', - localize('notebook.focusPreviousEditor', 'The Focus Previous Cell Editor command ({0}) will set focus in the previous cell\'s editor.'), - localize('notebook.focusPreviousEditorNoKb', 'The Focus Previous Cell Editor command will set focus in the previous cell\'s editor and is currently not triggerable by a keybinding.'), keybindingService)); - content.push(localize('notebook.cellNavigation', 'The up and down arrows will also move focus between cells while focused on the outer cell container.')); - content.push(descriptionForCommand('notebook.cell.executeAndFocusContainer', - localize('notebook.cell.executeAndFocusContainer', 'The Execute Cell command ({0}) executes the cell that currently has focus.',), - localize('notebook.cell.executeAndFocusContainerNoKb', 'The Execute Cell command executes the cell that currently has focus and is currently not triggerable by a keybinding.'), keybindingService)); - content.push(localize('notebook.cell.insertCodeCellBelowAndFocusContainer', 'The Insert Cell Above/Below commands will create new empty code cells')); - content.push(localize('notebook.changeCellType', 'The Change Cell to Code/Markdown commands are used to switch between cell types.')); - - - return content.join('\n\n'); -} - -function descriptionForCommand(commandId: string, msg: string, noKbMsg: string, keybindingService: IKeybindingService): string { - const kb = keybindingService.lookupKeybinding(commandId); - if (kb) { - return format(msg, kb.getAriaLabel()); - } - return format(noKbMsg, commandId); +export function getAccessibilityHelpText(): string { + return [ + localize('notebook.overview', 'The notebook view is a collection of code and markdown cells. Code cells can be executed and will produce output directly below the cell.'), + localize('notebook.cell.edit', 'The Edit Cell command will focus on the cell input.'), + localize('notebook.cell.quitEdit', 'The Quit Edit command will set focus on the cell container. The default (Escape) key may need to be pressed twice first exit the virtual cursor if active.'), + localize('notebook.cell.focusInOutput', 'The Focus Output command will set focus in the cell\'s output.'), + localize('notebook.focusNextEditor', 'The Focus Next Cell Editor command will set focus in the next cell\'s editor.'), + localize('notebook.focusPreviousEditor', 'The Focus Previous Cell Editor command will set focus in the previous cell\'s editor.'), + localize('notebook.cellNavigation', 'The up and down arrows will also move focus between cells while focused on the outer cell container.'), + localize('notebook.cell.executeAndFocusContainer', 'The Execute Cell command executes the cell that currently has focus.',), + localize('notebook.cell.insertCodeCellBelowAndFocusContainer', 'The Insert Cell Above/Below commands will create new empty code cells'), + localize('notebook.changeCellType', 'The Change Cell to Code/Markdown commands are used to switch between cell types.') + ].join('\n\n'); } export function runAccessibilityHelpAction(accessor: ServicesAccessor, editor: ICodeEditor | IVisibleEditorPane) { - const helpText = getAccessibilityHelpText(accessor); + const helpText = getAccessibilityHelpText(); return { id: AccessibleViewProviderId.Notebook, verbositySettingKey: AccessibilityVerbositySettingId.Notebook, From ddc97d4bffa21865d3ae4cfe28dc68d7969bd27d Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Mon, 20 May 2024 16:31:57 -0700 Subject: [PATCH 284/357] feat: support dynamic variables in chat context picker --- .../api/browser/mainThreadChatAgents2.ts | 15 ++++-- .../workbench/api/common/extHost.protocol.ts | 6 ++- .../api/common/extHostChatAgents2.ts | 4 +- .../api/common/extHostTypeConverters.ts | 2 + src/vs/workbench/api/common/extHostTypes.ts | 2 + .../browser/actions/chatContextActions.ts | 49 ++++++++++++++----- .../contrib/chat/browser/chatVariables.ts | 2 + .../contrib/chat/common/chatAgents.ts | 25 +++++++++- .../contrib/chat/common/chatModel.ts | 1 + .../contrib/chat/common/chatVariables.ts | 3 ++ .../chat/test/common/voiceChatService.test.ts | 4 +- ...ode.proposed.chatParticipantAdditions.d.ts | 2 + 12 files changed, 94 insertions(+), 21 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index e5ecb109309..0d63847dae5 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -10,6 +10,7 @@ import { IMarkdownString } from 'vs/base/common/htmlContent'; import { Disposable, DisposableMap, IDisposable } from 'vs/base/common/lifecycle'; import { revive } from 'vs/base/common/marshalling'; import { escapeRegExpCharacters } from 'vs/base/common/strings'; +import { ThemeIcon } from 'vs/base/common/themables'; import { URI, UriComponents } from 'vs/base/common/uri'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; @@ -73,6 +74,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA private readonly _agents = this._register(new DisposableMap()); private readonly _agentCompletionProviders = this._register(new DisposableMap()); + private readonly _agentIdsToCompletionProviders = this._register(new DisposableMap); private readonly _pendingProgress = new Map void>(); private readonly _proxy: ExtHostChatAgentsShape2; @@ -233,7 +235,13 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA this._pendingProgress.get(requestId)?.(revivedProgress); } - $registerAgentCompletionsProvider(handle: number, triggerCharacters: string[]): void { + $registerAgentCompletionsProvider(handle: number, id: string, triggerCharacters: string[]): void { + const provide = async (query: string, token: CancellationToken) => { + const completions = await this._proxy.$invokeCompletionProvider(handle, query, token); + return completions.map((c) => ({ ...c, icon: c.icon ? ThemeIcon.fromId(c.icon) : undefined })); + }; + this._agentIdsToCompletionProviders.set(id, this._chatAgentService.registerAgentCompletionProvider(id, provide)); + this._agentCompletionProviders.set(handle, this._languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { _debugDisplayName: 'chatAgentCompletions:' + handle, triggerCharacters, @@ -263,7 +271,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA return null; } - const result = await this._proxy.$invokeCompletionProvider(handle, query, token); + const result = await provide(query, token); const variableItems = result.map(v => { const insertText = v.insertText ?? (typeof v.label === 'string' ? v.label : v.label.label); const rangeAfterInsert = new Range(range.insert.startLineNumber, range.insert.startColumn, range.insert.endLineNumber, range.insert.startColumn + insertText.length); @@ -285,8 +293,9 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA })); } - $unregisterAgentCompletionsProvider(handle: number): void { + $unregisterAgentCompletionsProvider(handle: number, id: string): void { this._agentCompletionProviders.deleteAndDispose(handle); + this._agentIdsToCompletionProviders.deleteAndDispose(id); } } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 4f6c27c82e3..441388971aa 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1241,8 +1241,8 @@ export interface IDynamicChatAgentProps { export interface MainThreadChatAgentsShape2 extends IDisposable { $registerAgent(handle: number, extension: ExtensionIdentifier, id: string, metadata: IExtensionChatAgentMetadata, dynamicProps: IDynamicChatAgentProps | undefined): void; - $registerAgentCompletionsProvider(handle: number, triggerCharacters: string[]): void; - $unregisterAgentCompletionsProvider(handle: number): void; + $registerAgentCompletionsProvider(handle: number, id: string, triggerCharacters: string[]): void; + $unregisterAgentCompletionsProvider(handle: number, id: string): void; $updateAgent(handle: number, metadataUpdate: IExtensionChatAgentMetadata): void; $unregisterAgent(handle: number): void; $handleProgressChunk(requestId: string, chunk: IChatProgressDto, handle?: number): Promise; @@ -1252,6 +1252,8 @@ export interface MainThreadChatAgentsShape2 extends IDisposable { export interface IChatAgentCompletionItem { id: string; + fullName?: string; + icon?: string; insertText?: string; label: string | languages.CompletionItemLabel; value: IChatRequestVariableValueDto; diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index c39bf7741b0..09639dd0f3a 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -677,9 +677,9 @@ class ExtHostChatAgent { throw new Error('triggerCharacters are required'); } - that._proxy.$registerAgentCompletionsProvider(that._handle, v.triggerCharacters); + that._proxy.$registerAgentCompletionsProvider(that._handle, that.id, v.triggerCharacters); } else { - that._proxy.$unregisterAgentCompletionsProvider(that._handle); + that._proxy.$unregisterAgentCompletionsProvider(that._handle, that.id); } }, get participantVariableProvider() { diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index dcfbaba32ef..6ae92cd7ca3 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -2590,6 +2590,8 @@ export namespace ChatAgentCompletionItem { return { id: item.id, label: item.label, + fullName: item.fullName, + icon: item.icon?.id, value: item.values[0].value, insertText: item.insertText, detail: item.detail, diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 8ba1eae8425..189c90b8f63 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -4272,6 +4272,8 @@ export enum ChatVariableLevel { export class ChatCompletionItem implements vscode.ChatCompletionItem { id: string; label: string | CompletionItemLabel; + fullName?: string | undefined; + icon?: vscode.ThemeIcon; insertText?: string; values: vscode.ChatVariableValue[]; detail?: string; diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index a60487b7169..7d3c2770496 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -3,22 +3,26 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { CancellationToken } from 'vs/base/common/cancellation'; import { Codicon } from 'vs/base/common/codicons'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Schemas } from 'vs/base/common/network'; import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { Command } from 'vs/editor/common/languages'; import { localize, localize2 } from 'vs/nls'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { ICommandService } from 'vs/platform/commands/common/commands'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { AnythingQuickAccessProviderRunOptions } from 'vs/platform/quickinput/common/quickAccess'; import { IQuickInputService, IQuickPickItem, QuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; import { IChatWidget, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { SelectAndInsertFileAction } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables'; -import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatAgentLocation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { CONTEXT_CHAT_LOCATION, CONTEXT_IN_CHAT_INPUT } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { ChatRequestAgentPart } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import { AnythingQuickAccessProvider } from 'vs/workbench/contrib/search/browser/anythingQuickAccess'; @@ -56,20 +60,28 @@ class AttachContextAction extends Action2 { }); } - private _attachContext(widget: IChatWidget, ...picks: IQuickPickItem[]) { - widget?.attachContext(...picks.map((p) => ({ - fullName: p.label, - icon: 'icon' in p && ThemeIcon.isThemeIcon(p.icon) ? p.icon : undefined, - name: 'name' in p && typeof p.name === 'string' ? p.name : p.label, - value: 'resource' in p && URI.isUri(p.resource) ? p.resource : undefined, - id: 'id' in p && typeof p.id === 'string' ? p.id : - 'resource' in p && URI.isUri(p.resource) ? `${SelectAndInsertFileAction.Name}:${p.resource.toString()}` : '' - }))); + private async _attachContext(widget: IChatWidget, commandService: ICommandService, ...picks: any[]) { + const toAttach = []; + for (const pick of picks) { + if (pick && typeof pick === 'object' && 'command' in pick) { + const selection = await commandService.executeCommand(pick.command.id, ...(pick.command.arguments ?? [])); + if (selection) { + const qualifiedName = `${typeof pick.value === 'string' && pick.value.startsWith('#') ? pick.value.slice(1) : ''}${selection}`; + + toAttach.push({ ...pick, isDynamic: pick.isDynamic, value: pick.value, name: qualifiedName, fullName: `$(${pick.icon.id}) ${selection}` }); + } + } else { + toAttach.push({ ...pick, fullName: pick.label }); + } + } + widget?.attachContext(...toAttach); } override async run(accessor: ServicesAccessor, ...args: any[]): Promise { const quickInputService = accessor.get(IQuickInputService); + const chatAgentService = accessor.get(IChatAgentService); const chatVariablesService = accessor.get(IChatVariablesService); + const commandService = accessor.get(ICommandService); const widgetService = accessor.get(IChatWidgetService); const context: { widget?: IChatWidget } | undefined = args[0]; const widget = context?.widget ?? widgetService.lastFocusedWidget; @@ -77,13 +89,26 @@ class AttachContextAction extends Action2 { return; } - const quickPickItems: (QuickPickItem & { name?: string; icon?: ThemeIcon })[] = []; + const quickPickItems: (QuickPickItem & { isDynamic?: boolean; name?: string; icon?: ThemeIcon; command?: Command; value?: unknown })[] = []; for (const variable of chatVariablesService.getVariables()) { if (variable.fullName) { quickPickItems.push({ label: `${variable.icon ? `$(${variable.icon.id}) ` : ''}${variable.fullName}`, name: variable.name, id: variable.id, icon: variable.icon }); } } + if (widget.viewModel?.sessionId) { + const agentPart = widget.parsedInput.parts.find((part): part is ChatRequestAgentPart => part instanceof ChatRequestAgentPart); + if (agentPart) { + const completions = await chatAgentService.getAgentCompletionItems(agentPart.agent.id, '', CancellationToken.None); + for (const variable of completions) { + if (variable.fullName) { + quickPickItems.push({ label: `${variable.icon ? `$(${variable.icon.id}) ` : ''}${variable.fullName}`, id: variable.id, command: variable.command, icon: variable.icon, value: variable.value, isDynamic: true }); + } + } + } + + } + if (chatVariablesService.hasVariable(SelectAndInsertFileAction.Name)) { quickPickItems.push(SelectAndInsertFileAction.Item, { type: 'separator' }); } @@ -93,7 +118,7 @@ class AttachContextAction extends Action2 { placeholder: localize('chatContext.attach.placeholder', 'Search attachments'), providerOptions: { handleAccept: (item: IQuickPickItem) => { - this._attachContext(widget, item); + this._attachContext(widget, commandService, item); }, additionPicks: quickPickItems, includeSymbols: false, diff --git a/src/vs/workbench/contrib/chat/browser/chatVariables.ts b/src/vs/workbench/contrib/chat/browser/chatVariables.ts index 560f2f887e2..bdf8b8e2a5b 100644 --- a/src/vs/workbench/contrib/chat/browser/chatVariables.ts +++ b/src/vs/workbench/contrib/chat/browser/chatVariables.ts @@ -75,6 +75,8 @@ export class ChatVariablesService implements IChatVariablesService { resolvedVariables[i] = { id: data.data.id, modelDescription: data.data.modelDescription, name: attachment.name, range: attachment.range, value, references }; } }).catch(onUnexpectedExternalError)); + } else if (attachment.isDynamic) { + resolvedVariables[i] = { id: attachment.id, name: attachment.name, value: attachment.value }; } }); diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index 3fc918226a5..e006b9a7f2e 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -15,7 +15,7 @@ import { observableValue } from 'vs/base/common/observableInternal/base'; import { equalsIgnoreCase } from 'vs/base/common/strings'; import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; -import { ProviderResult } from 'vs/editor/common/languages'; +import { Command, ProviderResult } from 'vs/editor/common/languages'; import { ContextKeyExpr, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -145,6 +145,14 @@ interface IChatAgentEntry { impl?: IChatAgentImplementation; } +export interface IChatAgentCompletionItem { + id: string; + fullName?: string; + icon?: ThemeIcon; + value: unknown; + command?: Command; +} + export interface IChatAgentService { _serviceBrand: undefined; /** @@ -154,6 +162,8 @@ export interface IChatAgentService { registerAgent(id: string, data: IChatAgentData): IDisposable; registerAgentImplementation(id: string, agent: IChatAgentImplementation): IDisposable; registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable; + registerAgentCompletionProvider(id: string, provider: (query: string, token: CancellationToken) => Promise): IDisposable; + getAgentCompletionItems(id: string, query: string, 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; getAgent(id: string): IChatAgentData | undefined; @@ -255,6 +265,19 @@ export class ChatAgentService implements IChatAgentService { }); } + private _agentCompletionProviders = new Map Promise>(); + + registerAgentCompletionProvider(id: string, provider: (query: string, token: CancellationToken) => Promise) { + this._agentCompletionProviders.set(id, provider); + return { + dispose: () => { this._agentCompletionProviders.delete(id); } + }; + } + + async getAgentCompletionItems(id: string, query: string, token: CancellationToken) { + return await this._agentCompletionProviders.get(id)?.(query, token) ?? []; + } + updateAgent(id: string, updateMetadata: IChatAgentMetadata): void { const agent = this._getAgentEntry(id); if (!agent?.impl) { diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 895c910ed24..a28dd0b4640 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -32,6 +32,7 @@ export interface IChatRequestVariableEntry { range?: IOffsetRange; value: IChatRequestVariableValue; references?: IChatContentReference[]; + isDynamic?: boolean; } export interface IChatRequestVariableData { diff --git a/src/vs/workbench/contrib/chat/common/chatVariables.ts b/src/vs/workbench/contrib/chat/common/chatVariables.ts index 644cad67a79..09f9fd2396d 100644 --- a/src/vs/workbench/contrib/chat/common/chatVariables.ts +++ b/src/vs/workbench/contrib/chat/common/chatVariables.ts @@ -56,6 +56,9 @@ export interface IChatVariablesService { export interface IDynamicVariable { range: IRange; id: string; + fullName?: string; + icon?: ThemeIcon; + prefix?: string; modelDescription?: string; data: IChatRequestVariableValue; } diff --git a/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts b/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts index 2a2c9447a01..2ff31b0173c 100644 --- a/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts @@ -12,7 +12,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/uti import { ProviderResult } from 'vs/editor/common/languages'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; -import { ChatAgentLocation, IChatAgent, IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentImplementation, IChatAgentMetadata, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatAgentLocation, IChatAgent, IChatAgentCommand, IChatAgentCompletionItem, IChatAgentData, IChatAgentHistoryEntry, IChatAgentImplementation, IChatAgentMetadata, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChatProgress, IChatFollowup } from 'vs/workbench/contrib/chat/common/chatService'; import { IVoiceChatSessionOptions, IVoiceChatTextEvent, VoiceChatService } from 'vs/workbench/contrib/chat/common/voiceChatService'; @@ -68,6 +68,8 @@ suite('VoiceChat', () => { getAgentsByName(name: string): IChatAgentData[] { throw new Error('Method not implemented.'); } updateAgent(id: string, updateMetadata: IChatAgentMetadata): void { throw new Error('Method not implemented.'); } getAgentByFullyQualifiedId(id: string): IChatAgentData | undefined { throw new Error('Method not implemented.'); } + registerAgentCompletionProvider(id: string, provider: (query: string, token: CancellationToken) => Promise): IDisposable { throw new Error('Method not implemented.'); } + getAgentCompletionItems(id: string, query: string, token: CancellationToken): Promise { throw new Error('Method not implemented.'); } } class TestSpeechService implements ISpeechService { diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index ddd65ccbb54..2e89410250c 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -206,6 +206,8 @@ declare module 'vscode' { id: string; label: string | CompletionItemLabel; values: ChatVariableValue[]; + fullName?: string; + icon?: ThemeIcon; insertText?: string; detail?: string; documentation?: string | MarkdownString; From 9d170743690bd5f32c7b32f96850de8be4b23dac Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Mon, 20 May 2024 16:44:51 -0700 Subject: [PATCH 285/357] Fix misaligned jsdoc --- src/vs/platform/quickinput/common/quickAccess.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/platform/quickinput/common/quickAccess.ts b/src/vs/platform/quickinput/common/quickAccess.ts index 4e6e3e4e671..4fd3faf30e0 100644 --- a/src/vs/platform/quickinput/common/quickAccess.ts +++ b/src/vs/platform/quickinput/common/quickAccess.ts @@ -60,7 +60,7 @@ export interface IQuickAccessOptions { /** * Provider specific options for this particular showing of the * quick access. - */ + */ readonly providerOptions?: IQuickAccessProviderRunOptions; /** From 51591f83a103aec9acb2df4e64467d507107f776 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Mon, 20 May 2024 18:16:07 -0700 Subject: [PATCH 286/357] Fix shared input history between chat locations (#213111) Fix microsoft/vscode-copilot-release#1228 --- .../contrib/chat/browser/chatInputPart.ts | 4 ++-- .../chat/common/chatWidgetHistoryService.ts | 21 +++++++++++++------ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 17d6f47866a..ce0e53fa4ee 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -174,7 +174,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } setState(inputValue: string | undefined): void { - const history = this.historyService.getHistory(); + const history = this.historyService.getHistory(this.location); this.history = new HistoryNavigator(history, 50); if (typeof inputValue === 'string') { @@ -523,7 +523,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge saveState(): void { const inputHistory = this.history.getHistory(); - this.historyService.saveHistory(inputHistory); + this.historyService.saveHistory(this.location, inputHistory); } } diff --git a/src/vs/workbench/contrib/chat/common/chatWidgetHistoryService.ts b/src/vs/workbench/contrib/chat/common/chatWidgetHistoryService.ts index f9431569db7..01716421e7b 100644 --- a/src/vs/workbench/contrib/chat/common/chatWidgetHistoryService.ts +++ b/src/vs/workbench/contrib/chat/common/chatWidgetHistoryService.ts @@ -7,6 +7,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { Memento } from 'vs/workbench/common/memento'; +import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; import { CHAT_PROVIDER_ID } from 'vs/workbench/contrib/chat/common/chatParticipantContribTypes'; export interface IChatHistoryEntry { @@ -21,8 +22,8 @@ export interface IChatWidgetHistoryService { readonly onDidClearHistory: Event; clearHistory(): void; - getHistory(): IChatHistoryEntry[]; - saveHistory(history: IChatHistoryEntry[]): void; + getHistory(location: ChatAgentLocation): IChatHistoryEntry[]; + saveHistory(location: ChatAgentLocation, history: IChatHistoryEntry[]): void; } interface IChatHistory { @@ -51,15 +52,23 @@ export class ChatWidgetHistoryService implements IChatWidgetHistoryService { this.viewState = loadedState; } - getHistory(): IChatHistoryEntry[] { - return this.viewState.history?.[CHAT_PROVIDER_ID] ?? []; + getHistory(location: ChatAgentLocation): IChatHistoryEntry[] { + const key = this.getKey(location); + return this.viewState.history?.[key] ?? []; } - saveHistory(history: IChatHistoryEntry[]): void { + private getKey(location: ChatAgentLocation): string { + // Preserve history for panel by continuing to use the same old provider id. Use the location as a key for other chat locations. + return location === ChatAgentLocation.Panel ? CHAT_PROVIDER_ID : location; + } + + saveHistory(location: ChatAgentLocation, history: IChatHistoryEntry[]): void { if (!this.viewState.history) { this.viewState.history = {}; } - this.viewState.history[CHAT_PROVIDER_ID] = history; + + const key = this.getKey(location); + this.viewState.history[key] = history; this.memento.saveMemento(); } From 601018a7db0fdf5b46c0200d655044dc1cef1516 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Mon, 20 May 2024 20:44:31 -0700 Subject: [PATCH 287/357] Fix merging chat anchors in markdown content (#213114) * Fix merging multiple chat anchors inside markdown content * Simplify types * Fix tests --- .../contrib/chat/common/annotations.ts | 15 +++++++++------ .../contrib/chat/common/chatModel.ts | 19 ++++++++++++------- ...ctVulnerabilitiesFromText_multiline.0.snap | 3 ++- ...nerabilitiesFromText_multiple_vulns.0.snap | 3 ++- ...VulnerabilitiesFromText_single_line.0.snap | 3 ++- 5 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/annotations.ts b/src/vs/workbench/contrib/chat/common/annotations.ts index e531e55265a..8a57732c95d 100644 --- a/src/vs/workbench/contrib/chat/common/annotations.ts +++ b/src/vs/workbench/contrib/chat/common/annotations.ts @@ -6,13 +6,13 @@ import { MarkdownString } from 'vs/base/common/htmlContent'; import { basename } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { IRange } from 'vs/editor/common/core/range'; -import { IChatProgressRenderableResponseContent, IChatProgressResponseContent, canMergeMarkdownStrings } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IChatAgentMarkdownContentWithVulnerability, IChatAgentVulnerabilityDetails, IChatContentInlineReference, IChatMarkdownContent, IChatTask } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatProgressRenderableResponseContent, IChatProgressResponseContent, appendMarkdownString, canMergeMarkdownStrings } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatAgentVulnerabilityDetails, IChatMarkdownContent } from 'vs/workbench/contrib/chat/common/chatService'; export const contentRefUrl = 'http://_vscodecontentref_'; // must be lowercase for URI export function annotateSpecialMarkdownContent(response: ReadonlyArray): ReadonlyArray { - const result: Exclude[] = []; + const result: IChatProgressRenderableResponseContent[] = []; for (const item of response) { const previousItem = result[result.length - 1]; if (item.kind === 'inlineReference') { @@ -20,18 +20,21 @@ export function annotateSpecialMarkdownContent(response: ReadonlyArray${item.content.value}`; if (previousItem?.kind === 'markdownContent') { // Since this is inside a codeblock, it needs to be merged into the previous markdown content. - result[result.length - 1] = { content: new MarkdownString(previousItem.content.value + markdownText, { isTrusted: previousItem.content.isTrusted }), kind: 'markdownContent' }; + const merged = appendMarkdownString(previousItem.content, new MarkdownString(markdownText)); + result[result.length - 1] = { content: merged, kind: 'markdownContent' }; } else { result.push({ content: new MarkdownString(markdownText), kind: 'markdownContent' }); } diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index a28dd0b4640..500af3ab715 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -185,13 +185,7 @@ export class Response implements IResponse { // The last part can't be merged with- not markdown, or markdown with different permissions this._responseParts.push(progress); } else { - lastResponsePart.content = { - value: lastResponsePart.content.value + progress.content.value, - isTrusted: lastResponsePart.content.isTrusted, - supportThemeIcons: lastResponsePart.content.supportThemeIcons, - supportHtml: lastResponsePart.content.supportHtml, - baseUri: lastResponsePart.content.baseUri - } satisfies IMarkdownString; + lastResponsePart.content = appendMarkdownString(lastResponsePart.content, progress.content); } this._updateRepr(quiet); } else if (progress.kind === 'textEdit') { @@ -1018,3 +1012,14 @@ export function canMergeMarkdownStrings(md1: IMarkdownString, md2: IMarkdownStri md1.supportHtml === md2.supportHtml && md1.supportThemeIcons === md2.supportThemeIcons; } + +export function appendMarkdownString(md1: IMarkdownString, md2: IMarkdownString | string): IMarkdownString { + const appendedValue = typeof md2 === 'string' ? md2 : md2.value; + return { + value: md1.value + appendedValue, + isTrusted: md1.isTrusted, + supportThemeIcons: md1.supportThemeIcons, + supportHtml: md1.supportHtml, + baseUri: md1.baseUri + }; +} diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiline.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiline.0.snap index 11c9c2ef292..730cb160f96 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiline.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiline.0.snap @@ -4,7 +4,8 @@ value: "some code\nover\nmultiple lines content with vuln\nand\nnewlinesmore code\nwith newline", isTrusted: false, supportThemeIcons: false, - supportHtml: false + supportHtml: false, + baseUri: undefined }, kind: "markdownContent" } diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiple_vulns.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiple_vulns.0.snap index bc1d5cda51e..714c3f06ed6 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiple_vulns.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiple_vulns.0.snap @@ -4,7 +4,8 @@ value: "some code\nover\nmultiple lines content with vuln\nand\nnewlinesmore code\nwith newlinecontent with vuln\nand\nnewlines", isTrusted: false, supportThemeIcons: false, - supportHtml: false + supportHtml: false, + baseUri: undefined }, kind: "markdownContent" } diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_single_line.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_single_line.0.snap index 229ab7c6ac4..f68fd93b8bd 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_single_line.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_single_line.0.snap @@ -4,7 +4,8 @@ value: "some code content with vuln after", isTrusted: false, supportThemeIcons: false, - supportHtml: false + supportHtml: false, + baseUri: undefined }, kind: "markdownContent" } From 89eb8a767f25b7879a78929b1136aef8511612dc Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Tue, 21 May 2024 12:16:33 +0200 Subject: [PATCH 288/357] make sure to push an undo stop after each request (#213137) fixes https://github.com/microsoft/vscode-copilot/issues/5736 --- .../browser/inlineChatController.ts | 28 +++---- .../inlineChat/browser/inlineChatSession.ts | 10 ++- .../browser/inlineChatStrategies.ts | 78 ++++++++++--------- 3 files changed, 59 insertions(+), 57 deletions(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 007eab054be..1a1048c1db5 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -639,7 +639,7 @@ export class InlineChatController implements IEditorContribution { return State.ACCEPT; } - this._session.addInput(new SessionPrompt(input)); + this._session.addInput(new SessionPrompt(request)); return State.SHOW_REQUEST; } @@ -669,7 +669,6 @@ export class InlineChatController implements IEditorContribution { const progressiveEditsClock = StopWatch.create(); const progressiveEditsQueue = new Queue(); - let lastLength = 0; let message = Message.NONE; @@ -684,6 +683,8 @@ export class InlineChatController implements IEditorContribution { this._chatService.cancelCurrentRequestForSession(request.session.sessionId); })); + let lastLength = 0; + let isFirstChange = true; // apply edits store.add(response.onDidChange(() => { @@ -699,13 +700,6 @@ export class InlineChatController implements IEditorContribution { return; } - // if ("1") { - // return; - // } - - // TODO@jrieken - const editsShouldBeInstant = false; - const edits = response.response.value.map(part => { if (part.kind === 'textEditGroup' && isEqual(part.uri, this._session?.textModelN.uri)) { return part.edits; @@ -732,10 +726,12 @@ export class InlineChatController implements IEditorContribution { // influence the time it takes to receive the changes and progressive typing will // become infinitely fast for (const edits of newEdits) { - await this._makeChanges(edits, editsShouldBeInstant - ? undefined - : { duration: progressiveEditsAvgDuration.value, token: progressiveEditsCts.token } - ); + await this._makeChanges(edits, { + duration: progressiveEditsAvgDuration.value, + token: progressiveEditsCts.token + }, isFirstChange); + + isFirstChange = false; } // reshow the widget if the start position changed or shows at the wrong position @@ -981,7 +977,7 @@ export class InlineChatController implements IEditorContribution { } } - private async _makeChanges(edits: TextEdit[], opts: ProgressingEditsOptions | undefined) { + private async _makeChanges(edits: TextEdit[], opts: ProgressingEditsOptions | undefined, undoStopBefore: boolean) { assertType(this._session); assertType(this._strategy); @@ -1004,9 +1000,9 @@ export class InlineChatController implements IEditorContribution { this._inlineChatSavingService.markChanged(this._session); this._session.wholeRange.trackEdits(editOperations); if (opts) { - await this._strategy.makeProgressiveChanges(editOperations, editsObserver, opts); + await this._strategy.makeProgressiveChanges(editOperations, editsObserver, opts, undoStopBefore); } else { - await this._strategy.makeChanges(editOperations, editsObserver); + await this._strategy.makeChanges(editOperations, editsObserver, undoStopBefore); } this._ctxDidEdit.set(this._session.hasChangedText); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts index f0e890ebfae..6a6d05d7454 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts @@ -32,7 +32,7 @@ import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ILogService } from 'vs/platform/log/common/log'; -import { ChatModel, IChatResponseModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatModel, IChatRequestModel, IChatResponseModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; @@ -279,9 +279,13 @@ export class Session { export class SessionPrompt { + readonly value: string; + constructor( - readonly value: string, - ) { } + readonly request: IChatRequestModel + ) { + this.value = request.message.text; + } } export class SessionExchange { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts index 6f7727d6f17..2f1e2e33266 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts @@ -60,7 +60,6 @@ export abstract class EditModeStrategy { protected readonly _onDidAccept = this._store.add(new Emitter()); protected readonly _onDidDiscard = this._store.add(new Emitter()); - protected _editCount: number = 0; readonly onDidAccept: Event = this._onDidAccept.event; readonly onDidDiscard: Event = this._onDidDiscard.event; @@ -133,38 +132,9 @@ export abstract class EditModeStrategy { this._onDidDiscard.fire(); } - abstract makeProgressiveChanges(edits: ISingleEditOperation[], obs: IEditObserver, timings: ProgressingEditsOptions): Promise; + abstract makeProgressiveChanges(edits: ISingleEditOperation[], obs: IEditObserver, timings: ProgressingEditsOptions, undoStopBefore: boolean): Promise; - abstract makeChanges(edits: ISingleEditOperation[], obs: IEditObserver): Promise; - - protected async _makeChanges(edits: ISingleEditOperation[], obs: IEditObserver, opts: ProgressingEditsOptions | undefined, progress: Progress | undefined): Promise { - - // push undo stop before first edit - if (++this._editCount === 1) { - this._editor.pushUndoStop(); - } - - if (opts) { - // ASYNC - const durationInSec = opts.duration / 1000; - for (const edit of edits) { - const wordCount = countWords(edit.text ?? ''); - const speed = wordCount / durationInSec; - // console.log({ durationInSec, wordCount, speed: wordCount / durationInSec }); - const asyncEdit = asProgressiveEdit(new WindowIntervalTimer(this._zone.domNode), edit, speed, opts.token); - await performAsyncTextEdit(this._session.textModelN, asyncEdit, progress, obs); - } - - } else { - // SYNC - obs.start(); - this._session.textModelN.pushEditOperations(null, edits, (undoEdits) => { - progress?.report(undoEdits); - return null; - }); - obs.stop(); - } - } + abstract makeChanges(edits: ISingleEditOperation[], obs: IEditObserver, undoStopBefore: boolean): Promise; abstract undoChanges(altVersionId: number): Promise; @@ -216,10 +186,10 @@ export class PreviewStrategy extends EditModeStrategy { await super._doApplyChanges(false); } - override async makeChanges(edits: ISingleEditOperation[], obs: IEditObserver): Promise { + override async makeChanges(): Promise { } - override async makeProgressiveChanges(edits: ISingleEditOperation[], obs: IEditObserver, opts: ProgressingEditsOptions): Promise { + override async makeProgressiveChanges(): Promise { } override async undoChanges(altVersionId: number): Promise { @@ -289,6 +259,7 @@ export class LiveStrategy extends EditModeStrategy { private readonly _ctxCurrentChangeShowsDiff: IContextKey; private readonly _progressiveEditingDecorations: IEditorDecorationsCollection; + private _editCount: number = 0; override acceptHunk: () => Promise = () => super.acceptHunk(); override discardHunk: () => Promise = () => super.discardHunk(); @@ -347,11 +318,11 @@ export class LiveStrategy extends EditModeStrategy { await undoModelUntil(textModelN, altVersionId); } - override async makeChanges(edits: ISingleEditOperation[], obs: IEditObserver): Promise { - return this._makeChanges(edits, obs, undefined, undefined); + override async makeChanges(edits: ISingleEditOperation[], obs: IEditObserver, undoStopBefore: boolean): Promise { + return this._makeChanges(edits, obs, undefined, undefined, undoStopBefore); } - override async makeProgressiveChanges(edits: ISingleEditOperation[], obs: IEditObserver, opts: ProgressingEditsOptions): Promise { + override async makeProgressiveChanges(edits: ISingleEditOperation[], obs: IEditObserver, opts: ProgressingEditsOptions, undoStopBefore: boolean): Promise { // add decorations once per line that got edited const progress = new Progress(edits => { @@ -371,7 +342,38 @@ export class LiveStrategy extends EditModeStrategy { this._progressiveEditingDecorations.append(newDecorations); }); - return this._makeChanges(edits, obs, opts, progress); + return this._makeChanges(edits, obs, opts, progress, undoStopBefore); + } + + private async _makeChanges(edits: ISingleEditOperation[], obs: IEditObserver, opts: ProgressingEditsOptions | undefined, progress: Progress | undefined, undoStopBefore: boolean): Promise { + + // push undo stop before first edit + if (undoStopBefore) { + this._editor.pushUndoStop(); + } + + this._editCount++; + + if (opts) { + // ASYNC + const durationInSec = opts.duration / 1000; + for (const edit of edits) { + const wordCount = countWords(edit.text ?? ''); + const speed = wordCount / durationInSec; + // console.log({ durationInSec, wordCount, speed: wordCount / durationInSec }); + const asyncEdit = asProgressiveEdit(new WindowIntervalTimer(this._zone.domNode), edit, speed, opts.token); + await performAsyncTextEdit(this._session.textModelN, asyncEdit, progress, obs); + } + + } else { + // SYNC + obs.start(); + this._session.textModelN.pushEditOperations(null, edits, (undoEdits) => { + progress?.report(undoEdits); + return null; + }); + obs.stop(); + } } private readonly _hunkDisplayData = new Map(); From a3d7b649f4ff8f584ecb8db9a281796ff68ab020 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Tue, 21 May 2024 12:20:28 +0200 Subject: [PATCH 289/357] adjust max input part height for compact chat widget, keep input height separate when compute minHeight (#213139) fixes https://github.com/microsoft/vscode-copilot/issues/5707 --- src/vs/workbench/contrib/chat/browser/chatInputPart.ts | 7 +++++-- .../contrib/inlineChat/browser/inlineChatWidget.ts | 6 +++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index ce0e53fa4ee..f34ead61db1 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -96,6 +96,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private readonly _onDidChangeVisibility = this._register(new Emitter()); private readonly _contextResourceLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility.event }); + private readonly inputEditorMaxHeight: number; private inputEditorHeight = 0; private container!: HTMLElement; @@ -150,6 +151,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge ) { super(); + this.inputEditorMaxHeight = this.options.renderStyle === 'compact' ? INPUT_EDITOR_MAX_HEIGHT / 3 : INPUT_EDITOR_MAX_HEIGHT; + this.inputEditorHasText = CONTEXT_CHAT_INPUT_HAS_TEXT.bindTo(contextKeyService); this.chatCursorAtTop = CONTEXT_CHAT_INPUT_CURSOR_AT_TOP.bindTo(contextKeyService); this.inputEditorHasFocus = CONTEXT_CHAT_INPUT_HAS_FOCUS.bindTo(contextKeyService); @@ -326,7 +329,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._inputEditor = this._register(scopedInstantiationService.createInstance(CodeEditorWidget, this._inputEditorElement, options, editorOptions)); this._register(this._inputEditor.onDidChangeModelContent(() => { - const currentHeight = Math.min(this._inputEditor.getContentHeight(), INPUT_EDITOR_MAX_HEIGHT); + const currentHeight = Math.min(this._inputEditor.getContentHeight(), this.inputEditorMaxHeight); if (currentHeight !== this.inputEditorHeight) { this.inputEditorHeight = currentHeight; this._onDidChangeHeight.fire(); @@ -509,7 +512,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return { inputEditorBorder: 2, followupsHeight: this.followupsContainer.offsetHeight, - inputPartEditorHeight: Math.min(this._inputEditor.getContentHeight(), INPUT_EDITOR_MAX_HEIGHT), + inputPartEditorHeight: Math.min(this._inputEditor.getContentHeight(), this.inputEditorMaxHeight), inputPartHorizontalPadding: this.options.renderStyle === 'compact' ? 8 : 40, inputPartVerticalPadding: this.options.renderStyle === 'compact' ? 12 : 24, implicitContextHeight: this.attachedContextContainer.offsetHeight, diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index 96bf08b444a..e4cfccd30bc 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -383,17 +383,17 @@ export class InlineChatWidget { // The chat widget is variable height and supports scrolling. It should be // at least "maxWidgetHeight" high and at most the content height. - let maxWidgetHeight = 100; + let maxWidgetOutputHeight = 100; for (const item of this._chatWidget.viewModel?.getItems() ?? []) { if (isResponseVM(item) && item.response.value.some(r => r.kind === 'textEditGroup' && !r.state?.applied)) { - maxWidgetHeight = 270; + maxWidgetOutputHeight = 270; break; } } let value = this.contentHeight; value -= this._chatWidget.contentHeight; - value += Math.min(maxWidgetHeight, this._chatWidget.contentHeight); + value += Math.min(this._chatWidget.input.contentHeight + maxWidgetOutputHeight, this._chatWidget.contentHeight); return value; } From b903ddb27a0d05b9623c9df352deb76a8d6d4508 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Tue, 21 May 2024 15:55:08 +0200 Subject: [PATCH 290/357] chore - inline chat controller tests works without agents only (no more glue provider-agent) (#213151) --- .../contrib/chat/common/chatAgents.ts | 3 +- .../test/browser/inlineChatController.test.ts | 247 +++++------------- 2 files changed, 70 insertions(+), 180 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index e006b9a7f2e..989000af391 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { findLast } from 'vs/base/common/arraysFind'; import { timeout } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; @@ -288,7 +289,7 @@ export class ChatAgentService implements IChatAgentService { } getDefaultAgent(location: ChatAgentLocation): IChatAgent | undefined { - return this.getActivatedAgents().find(a => !!a.isDefault && a.locations.includes(location)); + return findLast(this.getActivatedAgents(), a => !!a.isDefault && a.locations.includes(location)); } getContributedDefaultAgent(location: ChatAgentLocation): IChatAgentData | undefined { 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 1c658e42c5a..3262ee01ccf 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts @@ -35,7 +35,7 @@ import { ChatAgentLocation, ChatAgentService, IChatAgentService } from 'vs/workb import { IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { InlineChatController, InlineChatRunOptions, State } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; import { Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; -import { CTX_INLINE_CHAT_USER_DID_EDIT, EditMode, IInlineChatRequest, IInlineChatService, InlineChatConfigKeys, InlineChatResponseType } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_INLINE_CHAT_USER_DID_EDIT, EditMode, IInlineChatService, InlineChatConfigKeys } 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 { IInlineChatSavingService } from '../../browser/inlineChatSavingService'; @@ -62,6 +62,20 @@ import { ICommandService } from 'vs/platform/commands/common/commands'; import { TestCommandService } from 'vs/editor/test/browser/editorTestServices'; suite('InteractiveChatController', function () { + + const agentData = { + extensionId: nullExtensionDescription.identifier, + publisherDisplayName: '', + extensionDisplayName: '', + extensionPublisherId: '', + // id: 'testEditorAgent', + name: 'testEditorAgent', + isDefault: true, + locations: [ChatAgentLocation.Editor], + metadata: {}, + slashCommands: [] + }; + class TestController extends InlineChatController { static INIT_SEQUENCE: readonly State[] = [State.CREATE_SESSION, State.INIT_UI, State.WAIT_FOR_INPUT]; @@ -112,7 +126,7 @@ suite('InteractiveChatController', function () { let model: ITextModel; let ctrl: TestController; let contextKeyService: MockContextKeyService; - let inlineChatService: InlineChatServiceImpl; + let chatAgentService: IChatAgentService; let inlineChatSessionService: IInlineChatSessionService; let instaService: TestInstantiationService; @@ -177,50 +191,27 @@ suite('InteractiveChatController', function () { contextKeyService = instaService.get(IContextKeyService) as MockContextKeyService; - inlineChatService = instaService.get(IInlineChatService) as InlineChatServiceImpl; + chatAgentService = instaService.get(IChatAgentService); - const chatAgentService = instaService.get(IChatAgentService); - - store.add(chatAgentService.registerDynamicAgent({ - extensionId: nullExtensionDescription.identifier, - publisherDisplayName: '', - extensionDisplayName: '', - extensionPublisherId: '', - id: 'testAgent', - name: 'testAgent', - isDefault: true, - locations: [ChatAgentLocation.Panel], - metadata: {}, - slashCommands: [] - }, { - async invoke(request, progress, history, token) { - return {}; - }, - })); inlineChatSessionService = store.add(instaService.get(IInlineChatSessionService)); model = store.add(instaService.get(IModelService).createModel('Hello\nWorld\nHello Again\nHello World\n', null)); editor = store.add(instantiateTestCodeEditor(instaService, model)); - store.add(inlineChatService.addProvider({ - extensionId: nullExtensionDescription.identifier, - label: 'Unit Test Default', - prepareInlineChatSession() { - return { - id: Math.random() - }; - }, - provideResponse(session, request) { - return { - type: InlineChatResponseType.EditorEdit, - id: Math.random(), + store.add(chatAgentService.registerDynamicAgent({ id: 'testEditorAgent', ...agentData, }, { + async invoke(request, progress, history, token) { + progress({ + kind: 'textEdit', + uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), - text: request.prompt + text: request.message }] - }; - } + }); + return {}; + }, })); + }); teardown(function () { @@ -255,48 +246,6 @@ suite('InteractiveChatController', function () { editor.setSelection(new Range(1, 1, 1, 3)); ctrl = instaService.createInstance(TestController, editor); - store.add(inlineChatService.addProvider({ - extensionId: nullExtensionDescription.identifier, - label: 'Unit Test', - prepareInlineChatSession() { - return { - id: Math.random() - }; - }, - provideResponse(session, request) { - throw new Error(); - } - })); - - ctrl.run({}); - await Event.toPromise(Event.filter(ctrl.onDidChangeState, e => e === State.WAIT_FOR_INPUT)); - - const session = inlineChatSessionService.getSession(editor, editor.getModel()!.uri); - assert.ok(session); - assert.deepStrictEqual(session.wholeRange.value, new Range(1, 1, 1, 3)); - - await ctrl.cancelSession(); - }); - - test('wholeRange expands to whole lines, session provided', async function () { - - editor.setSelection(new Range(1, 1, 1, 1)); - ctrl = instaService.createInstance(TestController, editor); - - store.add(inlineChatService.addProvider({ - extensionId: nullExtensionDescription.identifier, - label: 'Unit Test', - prepareInlineChatSession() { - return { - id: Math.random(), - wholeRange: new Range(1, 1, 1, 3) - }; - }, - provideResponse(session, request) { - throw new Error(); - } - })); - ctrl.run({}); await Event.toPromise(Event.filter(ctrl.onDidChangeState, e => e === State.WAIT_FOR_INPUT)); @@ -330,27 +279,24 @@ suite('InteractiveChatController', function () { test('\'whole range\' isn\'t updated for edits outside whole range #4346', async function () { - editor.setSelection(new Range(3, 1, 3, 1)); + editor.setSelection(new Range(3, 1, 3, 3)); - store.add(inlineChatService.addProvider({ - extensionId: nullExtensionDescription.identifier, - label: 'Unit Test', - prepareInlineChatSession() { - return { - id: Math.random(), - wholeRange: new Range(3, 1, 3, 3) - }; - }, - provideResponse(session, request) { - return { - type: InlineChatResponseType.EditorEdit, - id: Math.random(), + store.add(chatAgentService.registerDynamicAgent({ + id: 'testEditorAgent2', + ...agentData + }, { + async invoke(request, progress, history, token) { + progress({ + kind: 'textEdit', + uri: editor.getModel().uri, edits: [{ range: new Range(1, 1, 1, 1), // EDIT happens outside of whole range - text: `${request.prompt}\n${request.prompt}` + text: `${request.message}\n${request.message}` }] - }; - } + }); + + return {}; + }, })); ctrl = instaService.createInstance(TestController, editor); @@ -373,19 +319,16 @@ suite('InteractiveChatController', function () { }); test('Stuck inline chat widget #211', async function () { - store.add(inlineChatService.addProvider({ - extensionId: nullExtensionDescription.identifier, - label: 'Unit Test', - prepareInlineChatSession() { - return { - id: Math.random(), - wholeRange: new Range(3, 1, 3, 3) - }; - }, - provideResponse(session, request) { + + store.add(chatAgentService.registerDynamicAgent({ + id: 'testEditorAgent2', + ...agentData + }, { + async invoke(request, progress, history, token) { return new Promise(() => { }); - } + }, })); + ctrl = instaService.createInstance(TestController, editor); const p = ctrl.waitFor([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST]); const r = ctrl.run({ message: 'Hello', autoSend: true }); @@ -399,26 +342,18 @@ suite('InteractiveChatController', function () { test('[Bug] Inline Chat\'s streaming pushed broken iterations to the undo stack #2403', async function () { - store.add(inlineChatService.addProvider({ - extensionId: nullExtensionDescription.identifier, - label: 'Unit Test', - prepareInlineChatSession() { - return { - id: Math.random(), - wholeRange: new Range(3, 1, 3, 3) - }; + store.add(chatAgentService.registerDynamicAgent({ + id: 'testEditorAgent2', + ...agentData + }, { + async invoke(request, progress, history, token) { + + progress({ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: 'hEllo1\n' }] }); + progress({ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(2, 1, 2, 1), text: 'hEllo2\n' }] }); + progress({ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1000, 1), text: 'Hello1\nHello2\n' }] }); + + return {}; }, - async provideResponse(session, request, progress) { - - progress.report({ edits: [{ range: new Range(1, 1, 1, 1), text: 'hEllo1\n' }] }); - progress.report({ edits: [{ range: new Range(2, 1, 2, 1), text: 'hEllo2\n' }] }); - - return { - id: Math.random(), - type: InlineChatResponseType.EditorEdit, - edits: [{ range: new Range(1, 1, 1000, 1), text: 'Hello1\nHello2\n' }] - }; - } })); const valueThen = editor.getModel().getValue(); @@ -444,26 +379,22 @@ suite('InteractiveChatController', function () { return runWithFakedTimers({ maxTaskCount: Number.MAX_SAFE_INTEGER }, async () => { - store.add(inlineChatService.addProvider({ - extensionId: nullExtensionDescription.identifier, - label: 'Unit Test', - prepareInlineChatSession() { - return { - id: Math.random(), - }; - }, - async provideResponse(session, request, progress) { + store.add(chatAgentService.registerDynamicAgent({ + id: 'testEditorAgent2', + ...agentData + }, { + async invoke(request, progress, history, token) { const text = '${CSI}#a\n${CSI}#b\n${CSI}#c\n'; await timeout(10); - progress.report({ edits: [{ range: new Range(1, 1, 1, 1), text: text }] }); + progress({ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: text }] }); await timeout(10); - progress.report({ edits: [{ range: new Range(1, 1, 1, 1), text: text.repeat(1000) + 'DONE' }] }); + progress({ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: text.repeat(1000) + 'DONE' }] }); throw new Error('Too long'); - } + }, })); @@ -525,46 +456,4 @@ suite('InteractiveChatController', function () { assert.ok(model.getValue().includes('MANUAL')); }); - - test('context has correct preview document', async function () { - - const requests: IInlineChatRequest[] = []; - - store.add(inlineChatService.addProvider({ - extensionId: nullExtensionDescription.identifier, - label: 'Unit Test', - prepareInlineChatSession() { - return { - id: Math.random() - }; - }, - provideResponse(_session, request) { - requests.push(request); - return undefined; - } - })); - - async function makeRequest() { - const p = ctrl.waitFor(TestController.INIT_SEQUENCE_AUTO_SEND); - const r = ctrl.run({ message: 'Hello', autoSend: true }); - await p; - await ctrl.cancelSession(); - await r; - } - - // manual edits -> finish - ctrl = instaService.createInstance(TestController, editor); - - configurationService.setUserConfiguration('inlineChat', { mode: EditMode.Live }); - await makeRequest(); - - - configurationService.setUserConfiguration('inlineChat', { mode: EditMode.Preview }); - await makeRequest(); - - assert.strictEqual(requests.length, 2); - - assert.strictEqual(requests[0].previewDocument.toString(), model.uri.toString()); // live - assert.strictEqual(requests[1].previewDocument.toString(), model.uri.toString()); // preview (both use the same but edits aren't applied like that) - }); }); From 54d31be669cc7305539e15808e22481ef47dd990 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Tue, 21 May 2024 16:00:10 +0200 Subject: [PATCH 291/357] Tabs Multi Select Polish (#212930) * Tabs Multi Select Polish * :lipstick: * add todo * add todo * update * :lipstick: * :lipstick: * :lipstick: * more review * :lipstick: * :lipstick: * fix bad function call * remove todo * jsdoc * :lipstick: * fix typo * :lipstick: * :lipstick: * :lipstick: multiEditorTabs * model accepts multiple editors * Use setSelection() * fix tests * :lipstick: * :lipstick: * discussion * discussion * fix selection * typo --------- Co-authored-by: Benjamin Pasero --- .../parts/editor/editor.contribution.ts | 10 +- .../workbench/browser/parts/editor/editor.ts | 4 +- .../browser/parts/editor/editorActions.ts | 25 ++- .../browser/parts/editor/editorCommands.ts | 97 ++++------ .../browser/parts/editor/editorDropTarget.ts | 4 +- .../browser/parts/editor/editorGroupView.ts | 86 +++------ .../browser/parts/editor/editorTabsControl.ts | 4 +- .../parts/editor/editorTitleControl.ts | 4 +- .../parts/editor/media/editortabscontrol.css | 8 + .../parts/editor/multiEditorTabsControl.ts | 182 ++++++++++-------- .../parts/editor/multiRowEditorTabsControl.ts | 9 +- .../parts/editor/noEditorTabsControl.ts | 2 +- .../parts/editor/singleEditorTabsControl.ts | 2 +- src/vs/workbench/common/contextkeys.ts | 4 +- src/vs/workbench/common/editor.ts | 4 +- .../common/editor/editorGroupModel.ts | 107 +++------- .../files/browser/fileActions.contribution.ts | 6 +- .../workbench/contrib/files/browser/files.ts | 10 +- .../editor/common/editorGroupsService.ts | 31 +-- .../test/browser/editorGroupsService.test.ts | 27 +-- .../test/browser/workbenchTestServices.ts | 5 +- 21 files changed, 273 insertions(+), 358 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index 432513e4157..dfb20dc68fb 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -11,7 +11,7 @@ import { TextCompareEditorActiveContext, ActiveEditorPinnedContext, EditorGroupEditorsCountContext, ActiveEditorStickyContext, ActiveEditorAvailableEditorIdsContext, EditorPartMultipleEditorGroupsContext, ActiveEditorDirtyContext, ActiveEditorGroupLockedContext, ActiveEditorCanSplitInGroupContext, SideBySideEditorActiveContext, EditorTabsVisibleContext, ActiveEditorLastInGroupContext, EditorPartMaximizedEditorGroupContext, MultipleEditorGroupsContext, InEditorZenModeContext, - IsAuxiliaryEditorPartContext, ActiveCompareEditorCanSwapContext, MultipleEditorsSelectedContext + IsAuxiliaryEditorPartContext, ActiveCompareEditorCanSwapContext, MultipleEditorsSelectedInGroupContext } from 'vs/workbench/common/contextkeys'; import { SideBySideEditorInput, SideBySideEditorInputSerializer } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { TextResourceEditor } from 'vs/workbench/browser/parts/editor/textResourceEditor'; @@ -388,7 +388,7 @@ MenuRegistry.appendMenuItem(MenuId.EditorActionsPositionSubmenu, { command: { id // Editor Title Context Menu MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: CLOSE_EDITOR_COMMAND_ID, title: localize('close', "Close") }, group: '1_close', order: 10 }); MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: CLOSE_OTHER_EDITORS_IN_GROUP_COMMAND_ID, title: localize('closeOthers', "Close Others"), precondition: EditorGroupEditorsCountContext.notEqualsTo('1') }, group: '1_close', order: 20 }); -MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: CLOSE_EDITORS_TO_THE_RIGHT_COMMAND_ID, title: localize('closeRight', "Close to the Right"), precondition: ContextKeyExpr.and(ActiveEditorLastInGroupContext.toNegated(), MultipleEditorsSelectedContext.negate()) }, group: '1_close', order: 30, when: EditorTabsVisibleContext }); +MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: CLOSE_EDITORS_TO_THE_RIGHT_COMMAND_ID, title: localize('closeRight', "Close to the Right"), precondition: ContextKeyExpr.and(ActiveEditorLastInGroupContext.toNegated(), MultipleEditorsSelectedInGroupContext.negate()) }, group: '1_close', order: 30, when: EditorTabsVisibleContext }); MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: CLOSE_SAVED_EDITORS_COMMAND_ID, title: localize('closeAllSaved', "Close Saved") }, group: '1_close', order: 40 }); MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: CLOSE_EDITORS_IN_GROUP_COMMAND_ID, title: localize('closeAll', "Close All") }, group: '1_close', order: 50 }); MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: REOPEN_WITH_COMMAND_ID, title: localize('reopenWith', "Reopen Editor With...") }, group: '1_open', order: 10, when: ActiveEditorAvailableEditorIdsContext }); @@ -399,11 +399,11 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: SPLIT_ED MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: SPLIT_EDITOR_DOWN, title: localize('splitDown', "Split Down") }, group: '5_split', order: 20 }); MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: SPLIT_EDITOR_LEFT, title: localize('splitLeft', "Split Left") }, group: '5_split', order: 30 }); MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: SPLIT_EDITOR_RIGHT, title: localize('splitRight', "Split Right") }, group: '5_split', order: 40 }); -MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: SPLIT_EDITOR_IN_GROUP, title: localize('splitInGroup', "Split in Group"), precondition: MultipleEditorsSelectedContext.negate() }, group: '6_split_in_group', order: 10, when: ActiveEditorCanSplitInGroupContext }); -MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: JOIN_EDITOR_IN_GROUP, title: localize('joinInGroup', "Join in Group"), precondition: MultipleEditorsSelectedContext.negate() }, group: '6_split_in_group', order: 10, when: SideBySideEditorActiveContext }); +MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: SPLIT_EDITOR_IN_GROUP, title: localize('splitInGroup', "Split in Group"), precondition: MultipleEditorsSelectedInGroupContext.negate() }, group: '6_split_in_group', order: 10, when: ActiveEditorCanSplitInGroupContext }); +MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: JOIN_EDITOR_IN_GROUP, title: localize('joinInGroup', "Join in Group"), precondition: MultipleEditorsSelectedInGroupContext.negate() }, group: '6_split_in_group', order: 10, when: SideBySideEditorActiveContext }); MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: MOVE_EDITOR_INTO_NEW_WINDOW_COMMAND_ID, title: localize('moveToNewWindow', "Move into New Window") }, group: '7_new_window', order: 10 }); MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: COPY_EDITOR_INTO_NEW_WINDOW_COMMAND_ID, title: localize('copyToNewWindow', "Copy into New Window") }, group: '7_new_window', order: 20 }); -MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { submenu: MenuId.EditorTitleContextShare, title: localize('share', "Share"), group: '11_share', order: -1, when: MultipleEditorsSelectedContext.negate() }); +MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { submenu: MenuId.EditorTitleContextShare, title: localize('share', "Share"), group: '11_share', order: -1, when: MultipleEditorsSelectedInGroupContext.negate() }); // Editor Title Menu MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: TOGGLE_DIFF_SIDE_BY_SIDE, title: localize('inlineView', "Inline View"), toggled: ContextKeyExpr.equals('config.diffEditor.renderSideBySide', false) }, group: '1_diff', order: 10, when: ContextKeyExpr.has('isInDiffEditor') }); diff --git a/src/vs/workbench/browser/parts/editor/editor.ts b/src/vs/workbench/browser/parts/editor/editor.ts index 1bf1de43220..138541a6ecf 100644 --- a/src/vs/workbench/browser/parts/editor/editor.ts +++ b/src/vs/workbench/browser/parts/editor/editor.ts @@ -340,9 +340,9 @@ export interface IInternalEditorOpenOptions extends IInternalEditorTitleControlO readonly preserveWindowOrder?: boolean; /** - * Whether to add the editor to the selection or not. + * Inactive editors to select after opening the active selected editor. */ - readonly selected?: boolean; + readonly inactiveSelection?: EditorInput[]; } export interface IInternalEditorCloseOptions extends IInternalEditorTitleControlOptions { diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index 4ffd4ae7049..b4665e6d8b2 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -13,7 +13,7 @@ import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/bro import { GoFilter, IHistoryService } from 'vs/workbench/services/history/common/history'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { CLOSE_EDITOR_COMMAND_ID, MOVE_ACTIVE_EDITOR_COMMAND_ID, ActiveEditorMoveCopyArguments, SPLIT_EDITOR_LEFT, SPLIT_EDITOR_RIGHT, SPLIT_EDITOR_UP, SPLIT_EDITOR_DOWN, splitEditor, LAYOUT_EDITOR_GROUPS_COMMAND_ID, UNPIN_EDITOR_COMMAND_ID, COPY_ACTIVE_EDITOR_COMMAND_ID, SPLIT_EDITOR, resolveCommandsContext, getCommandsContext, TOGGLE_MAXIMIZE_EDITOR_GROUP, MOVE_EDITOR_INTO_NEW_WINDOW_COMMAND_ID, COPY_EDITOR_INTO_NEW_WINDOW_COMMAND_ID, MOVE_EDITOR_GROUP_INTO_NEW_WINDOW_COMMAND_ID, COPY_EDITOR_GROUP_INTO_NEW_WINDOW_COMMAND_ID, NEW_EMPTY_EDITOR_WINDOW_COMMAND_ID as NEW_EMPTY_EDITOR_WINDOW_COMMAND_ID, getEditorsFromContext } from 'vs/workbench/browser/parts/editor/editorCommands'; +import { CLOSE_EDITOR_COMMAND_ID, MOVE_ACTIVE_EDITOR_COMMAND_ID, ActiveEditorMoveCopyArguments, SPLIT_EDITOR_LEFT, SPLIT_EDITOR_RIGHT, SPLIT_EDITOR_UP, SPLIT_EDITOR_DOWN, splitEditor, LAYOUT_EDITOR_GROUPS_COMMAND_ID, UNPIN_EDITOR_COMMAND_ID, COPY_ACTIVE_EDITOR_COMMAND_ID, SPLIT_EDITOR, resolveCommandsContext, getCommandsContext, TOGGLE_MAXIMIZE_EDITOR_GROUP, MOVE_EDITOR_INTO_NEW_WINDOW_COMMAND_ID, COPY_EDITOR_INTO_NEW_WINDOW_COMMAND_ID, MOVE_EDITOR_GROUP_INTO_NEW_WINDOW_COMMAND_ID, COPY_EDITOR_GROUP_INTO_NEW_WINDOW_COMMAND_ID, NEW_EMPTY_EDITOR_WINDOW_COMMAND_ID as NEW_EMPTY_EDITOR_WINDOW_COMMAND_ID, resolveEditorsContext, getEditorsContext } from 'vs/workbench/browser/parts/editor/editorCommands'; import { IEditorGroupsService, IEditorGroup, GroupsArrangement, GroupLocation, GroupDirection, preferredSideBySideGroupDirection, IFindGroupScope, GroupOrientation, EditorGroupLayout, GroupsOrder, MergeGroupMode } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -65,7 +65,8 @@ abstract class AbstractSplitEditorAction extends Action2 { const editorGroupService = accessor.get(IEditorGroupsService); const configurationService = accessor.get(IConfigurationService); - splitEditor(editorGroupService, this.getDirection(configurationService), [getCommandsContext(accessor, resourceOrContext, context)]); + const commandContext = getCommandsContext(accessor, resourceOrContext, context); + splitEditor(editorGroupService, this.getDirection(configurationService), commandContext ? [commandContext] : undefined); } } @@ -2514,23 +2515,19 @@ abstract class BaseMoveCopyEditorToNewWindowAction extends Action2 { override async run(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) { const editorGroupService = accessor.get(IEditorGroupsService); - const editors = getEditorsFromContext(accessor, resourceOrContext, context); - - // If there is no editor, do not create a new window - if (editors.length === 0) { + const editorsContext = resolveEditorsContext(getEditorsContext(accessor, resourceOrContext, context)); + if (editorsContext.length === 0) { return; } const auxiliaryEditorPart = await editorGroupService.createAuxiliaryEditorPart(); - for (const { editor, group } of editors) { - if (group && editor) { - if (this.move) { - group.moveEditor(editor, auxiliaryEditorPart.activeGroup); - } else { - group.copyEditor(editor, auxiliaryEditorPart.activeGroup); - } - } + const sourceGroup = editorsContext[0].group; // only single group supported for move/copy for now + const sourceEditors = editorsContext.filter(({ group }) => group === sourceGroup); + if (this.move) { + sourceGroup.moveEditors(sourceEditors, auxiliaryEditorPart.activeGroup); + } else { + sourceGroup.copyEditors(sourceEditors, auxiliaryEditorPart.activeGroup); } auxiliaryEditorPart.activeGroup.focus(); diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index 848096c7358..c8670de6fb0 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -31,7 +31,7 @@ import { ActiveGroupEditorsByMostRecentlyUsedQuickAccess } from 'vs/workbench/br import { SideBySideEditor } from 'vs/workbench/browser/parts/editor/sideBySideEditor'; import { TextDiffEditor } from 'vs/workbench/browser/parts/editor/textDiffEditor'; import { ActiveEditorCanSplitInGroupContext, ActiveEditorGroupEmptyContext, ActiveEditorGroupLockedContext, ActiveEditorStickyContext, MultipleEditorGroupsContext, SideBySideEditorActiveContext, TextCompareEditorActiveContext } from 'vs/workbench/common/contextkeys'; -import { CloseDirection, EditorInputCapabilities, EditorsOrder, IEditorCommandsContext, IEditorIdentifier, IResourceDiffEditorInput, IUntitledTextResourceEditorInput, IVisibleEditorPane, isEditorCommandsContext, isEditorIdentifier, isEditorInputWithOptionsAndGroup } from 'vs/workbench/common/editor'; +import { CloseDirection, EditorInputCapabilities, EditorsOrder, IEditorCommandsContext, IEditorIdentifier, IResourceDiffEditorInput, IUntitledTextResourceEditorInput, IVisibleEditorPane, isEditorIdentifier, isEditorInputWithOptionsAndGroup } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; @@ -656,48 +656,45 @@ function registerFocusEditorGroupAtIndexCommands(): void { } } -export function splitEditor(editorGroupService: IEditorGroupsService, direction: GroupDirection, contexts?: (IEditorCommandsContext | URI | undefined)[]): void { +export function splitEditor(editorGroupService: IEditorGroupsService, direction: GroupDirection, contexts?: IEditorCommandsContext[]): void { let newGroup: IEditorGroup | undefined; + let sourceGroup: IEditorGroup | undefined; for (const context of contexts ?? [undefined]) { - let sourceGroup: IEditorGroup | undefined; + let currentGroup: IEditorGroup | undefined; - const isEditorCommand = context && isEditorCommandsContext(context); - const isURI = context && URI.isUri(context); - - if (isEditorCommand) { - sourceGroup = editorGroupService.getGroup(context.groupId); - } else if (isURI) { - for (const group of editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE)) { - if (isEqual(group.activeEditor?.resource, context)) { - sourceGroup = group; - break; - } - } + if (context) { + currentGroup = editorGroupService.getGroup(context.groupId); } else { - sourceGroup = editorGroupService.activeGroup; + currentGroup = editorGroupService.activeGroup; + } + + if (!currentGroup) { + continue; } if (!sourceGroup) { - return; + sourceGroup = currentGroup; + } else if (sourceGroup.id !== currentGroup.id) { + continue; // Only support splitting from the same group } // Add group if (!newGroup) { - newGroup = editorGroupService.addGroup(sourceGroup, direction); + newGroup = editorGroupService.addGroup(currentGroup, direction); } // Split editor (if it can be split) let editorToCopy: EditorInput | undefined; - if (isEditorCommand && typeof context.editorIndex === 'number') { - editorToCopy = sourceGroup.getEditorByIndex(context.editorIndex); + if (context && typeof context.editorIndex === 'number') { + editorToCopy = currentGroup.getEditorByIndex(context.editorIndex); } else { - editorToCopy = sourceGroup.activeEditor ?? undefined; + editorToCopy = currentGroup.activeEditor ?? undefined; } // Copy the editor to the new group, else create an empty group if (editorToCopy && !editorToCopy.hasCapability(EditorInputCapabilities.Singleton)) { - sourceGroup.copyEditor(editorToCopy, newGroup, { preserveFocus: isEditorCommand && context?.preserveFocus }); + currentGroup.copyEditor(editorToCopy, newGroup, { preserveFocus: context?.preserveFocus }); } } @@ -897,7 +894,7 @@ function registerCloseEditorCommands() { const editorResolverService = accessor.get(IEditorResolverService); const telemetryService = accessor.get(ITelemetryService); - const editorsAndGroup = getEditorsFromContext(accessor, resourceOrContext, context); + const editorsAndGroup = resolveEditorsContext(getEditorsContext(accessor, resourceOrContext, context)); const editorReplacements = new Map(); for (const { editor, group } of editorsAndGroup) { @@ -912,11 +909,13 @@ function registerCloseEditorCommands() { return; } - if (!editorReplacements.has(group)) { - editorReplacements.set(group, []); + let editorReplacementsInGroup = editorReplacements.get(group); + if (!editorReplacementsInGroup) { + editorReplacementsInGroup = []; + editorReplacements.set(group, editorReplacementsInGroup); } - editorReplacements.get(group)?.push({ + editorReplacementsInGroup.push({ editor: editor, replacement: resolvedEditor.editor, forceReplaceDirty: editor.resource?.scheme === Schemas.untitled, @@ -948,18 +947,11 @@ function registerCloseEditorCommands() { }); } - // Replace editor with resolved one - let group: IEditorGroup | undefined, replacements: IEditorReplacement[] | undefined; - for ([group, replacements] of editorReplacements) { + // Replace editor with resolved one and make active + for (const [group, replacements] of editorReplacements) { await group.replaceEditors(replacements); + await group.openEditor(replacements[0].replacement); } - - if (!group || !replacements) { - return; - } - - // Make sure it becomes active too - await group?.openEditor(replacements[0].replacement); } }); @@ -1301,7 +1293,7 @@ function registerOtherEditorCommands(): void { when: ActiveEditorStickyContext.toNegated(), primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.Shift | KeyCode.Enter), handler: async (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { - for (const { editor, group } of getEditorsFromContext(accessor, resourceOrContext, context)) { + for (const { editor, group } of resolveEditorsContext(getEditorsContext(accessor, resourceOrContext, context))) { group.stickEditor(editor); } } @@ -1340,7 +1332,7 @@ function registerOtherEditorCommands(): void { when: ActiveEditorStickyContext, primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.Shift | KeyCode.Enter), handler: async (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { - for (const { editor, group } of getEditorsFromContext(accessor, resourceOrContext, context)) { + for (const { editor, group } of resolveEditorsContext(getEditorsContext(accessor, resourceOrContext, context))) { group.unstickEditor(editor); } } @@ -1368,7 +1360,8 @@ function registerOtherEditorCommands(): void { }); } -function getEditorsContext(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): { editors: IEditorCommandsContext[]; groups: Array } { +type EditorsContext = { editors: IEditorCommandsContext[]; groups: Array }; +export function getEditorsContext(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): EditorsContext { const editorGroupService = accessor.get(IEditorGroupsService); const listService = accessor.get(IListService); @@ -1389,8 +1382,8 @@ function getEditorsContext(accessor: ServicesAccessor, resourceOrContext?: URI | }; } -export function getEditorsFromContext(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): { editor: EditorInput; group: IEditorGroup }[] { - const { editors, groups } = getEditorsContext(accessor, resourceOrContext, context); +export function resolveEditorsContext(context: EditorsContext): { editor: EditorInput; group: IEditorGroup }[] { + const { editors, groups } = context; const editorsAndGroup = editors.map(e => { if (e.editorIndex === undefined) { @@ -1404,14 +1397,14 @@ export function getEditorsFromContext(accessor: ServicesAccessor, resourceOrCont return { editor, group }; }); - return editorsAndGroup.filter(group => !!group); + return coalesce(editorsAndGroup); } export function getCommandsContext(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): IEditorCommandsContext | undefined { const isUri = URI.isUri(resourceOrContext); const editorCommandsContext = isUri ? context : resourceOrContext ? resourceOrContext : context; - if (editorCommandsContext && typeof editorCommandsContext.groupId === 'number') { + if (editorCommandsContext) { return editorCommandsContext; } @@ -1478,19 +1471,11 @@ export function getMultiSelectedEditorContexts(editorContext: IEditorCommandsCon // Check editors selected in the group (tabs) else { const group = editorContext ? editorGroupService.getGroup(editorContext.groupId) : editorGroupService.activeGroup; - if (group) { - const selectedEditors: EditorInput[] = []; - // If context provides an editor index, use it - if (editorContext && editorContext.editorIndex !== undefined) { - const editor = group?.getEditorByIndex(editorContext.editorIndex); - if (editor && group.isSelected(editor)) { - selectedEditors.push(...group.selectedEditors); - } - } else { - selectedEditors.push(...group.selectedEditors); - } - if (selectedEditors.length > 1) { - return selectedEditors.map(se => ({ groupId: group.id, editorIndex: group.getIndexOfEditor(se) })); + const editor = editorContext && editorContext.editorIndex !== undefined ? group?.getEditorByIndex(editorContext.editorIndex) : group?.activeEditor; + // If the editor is selected, return all selected editors otherwise only use the editors context + if (group && editor) { + if (group.isSelected(editor)) { + return group.selectedEditors.map(se => ({ groupId: group.id, editorIndex: group.getIndexOfEditor(se) })); } } } diff --git a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts index 002d565c6cd..463b527e3b6 100644 --- a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts +++ b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts @@ -333,8 +333,8 @@ class DropOverlay extends Themable { { editor: draggedEditor.identifier.editor, options: fillActiveEditorViewState(sourceGroup, draggedEditor.identifier.editor, { - pinned: true, // always pin dropped editor - sticky: sourceGroup.isSticky(firstDraggedEditor.editor) // preserve sticky state + pinned: true, // always pin dropped editor + sticky: sourceGroup.isSticky(draggedEditor.identifier.editor) // preserve sticky state }) } )); diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 330c90fd1ae..14beefacad6 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/editorgroupview'; import { EditorGroupModel, IEditorOpenOptions, IGroupModelChangeEvent, ISerializedEditorGroupModel, isGroupEditorCloseEvent, isGroupEditorOpenEvent, isSerializedEditorGroupModel } from 'vs/workbench/common/editor/editorGroupModel'; import { GroupIdentifier, CloseDirection, IEditorCloseEvent, IEditorPane, SaveReason, IEditorPartOptionsChangeEvent, EditorsOrder, IVisibleEditorPane, EditorResourceAccessor, EditorInputCapabilities, IUntypedEditorInput, DEFAULT_EDITOR_ASSOCIATION, SideBySideEditor, EditorCloseContext, IEditorWillMoveEvent, IEditorWillOpenEvent, IMatchEditorOptions, GroupModelChangeKind, IActiveEditorChangeEvent, IFindEditorOptions, IToolbarActions, TEXT_DIFF_EDITOR_ID } from 'vs/workbench/common/editor'; -import { ActiveEditorGroupLockedContext, ActiveEditorDirtyContext, EditorGroupEditorsCountContext, ActiveEditorStickyContext, ActiveEditorPinnedContext, ActiveEditorLastInGroupContext, ActiveEditorFirstInGroupContext, ResourceContextKey, applyAvailableEditorIds, ActiveEditorAvailableEditorIdsContext, ActiveEditorCanSplitInGroupContext, SideBySideEditorActiveContext, MultipleEditorsSelectedContext, TwoEditorsSelectedContext, TextCompareEditorVisibleContext, TextCompareEditorActiveContext, ActiveEditorContext, ActiveEditorReadonlyContext, ActiveEditorCanRevertContext, ActiveEditorCanToggleReadonlyContext, ActiveCompareEditorCanSwapContext } from 'vs/workbench/common/contextkeys'; +import { ActiveEditorGroupLockedContext, ActiveEditorDirtyContext, EditorGroupEditorsCountContext, ActiveEditorStickyContext, ActiveEditorPinnedContext, ActiveEditorLastInGroupContext, ActiveEditorFirstInGroupContext, ResourceContextKey, applyAvailableEditorIds, ActiveEditorAvailableEditorIdsContext, ActiveEditorCanSplitInGroupContext, SideBySideEditorActiveContext, TextCompareEditorVisibleContext, TextCompareEditorActiveContext, ActiveEditorContext, ActiveEditorReadonlyContext, ActiveEditorCanRevertContext, ActiveEditorCanToggleReadonlyContext, ActiveCompareEditorCanSwapContext, MultipleEditorsSelectedInGroupContext, TwoEditorsSelectedInGroupContext } from 'vs/workbench/common/contextkeys'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { Emitter, Relay } from 'vs/base/common/event'; @@ -256,8 +256,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView { const groupEditorsCountContext = this.editorPartsView.bind(EditorGroupEditorsCountContext, this); const groupLockedContext = this.editorPartsView.bind(ActiveEditorGroupLockedContext, this); - const multipleEditorsSelectedContext = MultipleEditorsSelectedContext.bindTo(this.scopedContextKeyService); - const twoEditorsSelectedContext = TwoEditorsSelectedContext.bindTo(this.scopedContextKeyService); + const multipleEditorsSelectedContext = MultipleEditorsSelectedInGroupContext.bindTo(this.scopedContextKeyService); + const twoEditorsSelectedContext = TwoEditorsSelectedInGroupContext.bindTo(this.scopedContextKeyService); const groupActiveEditorContext = this.editorPartsView.bind(ActiveEditorContext, this); const groupActiveEditorIsReadonly = this.editorPartsView.bind(ActiveEditorReadonlyContext, this); @@ -349,7 +349,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { groupActiveEditorStickyContext.set(this.model.isSticky(this.model.activeEditor)); } break; - case GroupModelChangeKind.EDITOR_SELECTION: + case GroupModelChangeKind.EDITORS_SELECTION: multipleEditorsSelectedContext.set(this.model.selectedEditors.length > 1); twoEditorsSelectedContext.set(this.model.selectedEditors.length === 2); break; @@ -599,8 +599,13 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Handle within - if (e.kind === GroupModelChangeKind.GROUP_LOCKED) { - this.element.classList.toggle('locked', this.isLocked); + switch (e.kind) { + case GroupModelChangeKind.GROUP_LOCKED: + this.element.classList.toggle('locked', this.isLocked); + break; + case GroupModelChangeKind.EDITORS_SELECTION: + this.onDidChangeEditorSelection(); + break; } if (!e.editor) { @@ -630,9 +635,6 @@ export class EditorGroupView extends Themable implements IEditorGroupView { case GroupModelChangeKind.EDITOR_LABEL: this.onDidChangeEditorLabel(e.editor); break; - case GroupModelChangeKind.EDITOR_SELECTION: - this.onDidChangeEditorSelection(e.editor); - break; } } @@ -863,10 +865,10 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.titleControl.updateEditorLabel(editor); } - private onDidChangeEditorSelection(editor: EditorInput): void { + private onDidChangeEditorSelection(): void { // Forward to title control - this.titleControl.setEditorSelections([editor], this.model.isSelected(editor)); + this.titleControl.updateEditorSelections(); } private onDidVisibilityChange(visible: boolean): void { @@ -941,6 +943,11 @@ export class EditorGroupView extends Themable implements IEditorGroupView { setActive(isActive: boolean): void { this.active = isActive; + // Clear selection when group no longer active + if (!isActive && this.activeEditor && this.selectedEditors.length > 1) { + this.setSelection(this.activeEditor, []); + } + // Update container this.element.classList.toggle('active', isActive); this.element.classList.toggle('inactive', !isActive); @@ -1015,55 +1022,14 @@ export class EditorGroupView extends Themable implements IEditorGroupView { return this.model.isActive(editor); } - async selectEditor(editor: EditorInput, active?: boolean): Promise { - if (active) { - await this.doOpenEditor(editor, { activation: EditorActivation.ACTIVATE }, { selected: true }); + async setSelection(activeSelectedEditor: EditorInput, inactiveSelectedEditors: EditorInput[]): Promise { + if (!this.isActive(activeSelectedEditor)) { + // The active selected editor is not yet opened, so we go + // through `openEditor` to show it. We pass the inactive + // selection as internal options + await this.openEditor(activeSelectedEditor, { activation: EditorActivation.ACTIVATE }, { inactiveSelection: inactiveSelectedEditors }); } else { - this.model.selectEditor(editor, active); - } - } - - async selectEditors(editors: EditorInput[], activeEditor?: EditorInput): Promise { - for (const editor of editors) { - await this.selectEditor(editor, editor === activeEditor); - } - } - - async unSelectEditor(editor: EditorInput): Promise { - await this.unSelectEditors([editor]); - } - - async unSelectEditors(editors: EditorInput[]): Promise { - // Check if the active editor is unselected - const unselectingActiveEditor = !!editors.find(editor => this.model.isActive(editor)); - if (unselectingActiveEditor) { - editors = editors.filter(editor => !this.model.isActive(editor)); - } - - // Unselect all none active editors - for (const editor of editors) { - this.model.unselectEditor(editor); - } - - // if the active editor is unselected, make another selected editor active - if (unselectingActiveEditor) { - // do not allow to unselect the active editor if it is the last selected editor - if (this.selectedEditors.length === 1) { - console.warn('Cannot unselect the last selected editor of a group'); - return; - } - - const activeEditor = this.activeEditor; - // Find the next selected editor to make active based on MRU order - const recentEditors = this.model.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE); - for (let i = 1; i < recentEditors.length; i++) { // First one is the active editor - const recentEditor = recentEditors[i]; - if (this.isSelected(recentEditor)) { - await this.doOpenEditor(recentEditor, { activation: EditorActivation.ACTIVATE }, { selected: true }); - this.model.unselectEditor(activeEditor!); - break; - } - } + this.model.setSelection(activeSelectedEditor, inactiveSelectedEditors); } } @@ -1217,7 +1183,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { pinned, sticky: options?.sticky || (typeof options?.index === 'number' && this.model.isSticky(options.index)), transient: !!options?.transient, - selected: internalOptions?.selected, + inactiveSelection: internalOptions?.inactiveSelection, active: this.count === 0 || !options || !options.inactive, supportSideBySide: internalOptions?.supportSideBySide }; diff --git a/src/vs/workbench/browser/parts/editor/editorTabsControl.ts b/src/vs/workbench/browser/parts/editor/editorTabsControl.ts index e006ebd9040..e5e4f7774de 100644 --- a/src/vs/workbench/browser/parts/editor/editorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/editorTabsControl.ts @@ -86,7 +86,7 @@ export interface IEditorTabsControl extends IDisposable { stickEditor(editor: EditorInput): void; unstickEditor(editor: EditorInput): void; setActive(isActive: boolean): void; - setEditorSelections(editors: EditorInput[], selected: boolean): void; + updateEditorSelections(): void; updateEditorLabel(editor: EditorInput): void; updateEditorDirty(editor: EditorInput): void; layout(dimensions: IEditorTitleControlDimensions): Dimension; @@ -503,7 +503,7 @@ export abstract class EditorTabsControl extends Themable implements IEditorTabsC abstract setActive(isActive: boolean): void; - abstract setEditorSelections(editors: EditorInput[], selected: boolean): void; + abstract updateEditorSelections(): void; abstract updateEditorLabel(editor: EditorInput): void; diff --git a/src/vs/workbench/browser/parts/editor/editorTitleControl.ts b/src/vs/workbench/browser/parts/editor/editorTitleControl.ts index e24e15ed611..d134e9b6175 100644 --- a/src/vs/workbench/browser/parts/editor/editorTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/editorTitleControl.ts @@ -163,8 +163,8 @@ export class EditorTitleControl extends Themable { return this.editorTabsControl.setActive(isActive); } - setEditorSelections(editors: EditorInput[], selected: boolean): void { - this.editorTabsControl.setEditorSelections(editors, selected); + updateEditorSelections(): void { + this.editorTabsControl.updateEditorSelections(); } updateEditorLabel(editor: EditorInput): void { diff --git a/src/vs/workbench/browser/parts/editor/media/editortabscontrol.css b/src/vs/workbench/browser/parts/editor/media/editortabscontrol.css index bf44c02aee2..24de0a1f384 100644 --- a/src/vs/workbench/browser/parts/editor/media/editortabscontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/editortabscontrol.css @@ -46,4 +46,12 @@ border-radius: 10px; font-size: 12px; position: absolute; + /* + * Browsers apply an effect to the drag image when the div becomes too + * large which makes them unreadable. Use max width so it does not happen + */ + max-width: 120px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } diff --git a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts index aa6a35fffcd..a54b4a431d3 100644 --- a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts @@ -27,12 +27,12 @@ import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import { getOrSet } from 'vs/base/common/map'; import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { TAB_INACTIVE_BACKGROUND, TAB_ACTIVE_BACKGROUND, TAB_BORDER, EDITOR_DRAG_AND_DROP_BACKGROUND, TAB_UNFOCUSED_ACTIVE_BACKGROUND, TAB_UNFOCUSED_ACTIVE_BORDER, TAB_ACTIVE_BORDER, TAB_HOVER_BACKGROUND, TAB_HOVER_BORDER, TAB_UNFOCUSED_HOVER_BACKGROUND, TAB_UNFOCUSED_HOVER_BORDER, EDITOR_GROUP_HEADER_TABS_BACKGROUND, WORKBENCH_BACKGROUND, TAB_ACTIVE_BORDER_TOP, TAB_UNFOCUSED_ACTIVE_BORDER_TOP, TAB_ACTIVE_MODIFIED_BORDER, TAB_INACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_ACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_INACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_INACTIVE_BACKGROUND, TAB_HOVER_FOREGROUND, TAB_UNFOCUSED_HOVER_FOREGROUND, EDITOR_GROUP_HEADER_TABS_BORDER, TAB_LAST_PINNED_BORDER, TAB_SELECTED_BORDER_TOP } from 'vs/workbench/common/theme'; -import { activeContrastBorder, contrastBorder, editorBackground } from 'vs/platform/theme/common/colorRegistry'; +import { activeContrastBorder, contrastBorder, editorBackground, listActiveSelectionBackground, listActiveSelectionForeground } from 'vs/platform/theme/common/colorRegistry'; import { ResourcesDropHandler, DraggedEditorIdentifier, DraggedEditorGroupIdentifier, extractTreeDropData, isWindowDraggedOver } from 'vs/workbench/browser/dnd'; import { Color } from 'vs/base/common/color'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { MergeGroupMode, IMergeGroupOptions } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { addDisposableListener, EventType, EventHelper, Dimension, scheduleAtNextAnimationFrame, findParentWithClass, clearNode, DragAndDropObserver, isMouseEvent, getWindow, $, getActiveDocument } from 'vs/base/browser/dom'; +import { addDisposableListener, EventType, EventHelper, Dimension, scheduleAtNextAnimationFrame, findParentWithClass, clearNode, DragAndDropObserver, isMouseEvent, getWindow } from 'vs/base/browser/dom'; import { localize } from 'vs/nls'; import { IEditorGroupsView, EditorServiceImpl, IEditorGroupView, IInternalEditorOpenOptions, IEditorPartsView } from 'vs/workbench/browser/parts/editor/editor'; import { CloseOneEditorAction, UnpinEditorAction } from 'vs/workbench/browser/parts/editor/editorActions'; @@ -57,6 +57,7 @@ import { StickyEditorGroupModel, UnstickyEditorGroupModel } from 'vs/workbench/c import { IReadonlyEditorGroupModel } from 'vs/workbench/common/editor/editorGroupModel'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { BugIndicatingError } from 'vs/base/common/errors'; +import { applyDragImage } from 'vs/base/browser/dnd'; interface IEditorInputLabel { readonly editor: EditorInput; @@ -683,7 +684,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { this.layout(this.dimensions, { forceRevealActiveTab: true }); } - setEditorSelections(editors: EditorInput[], selected: boolean): void { + updateEditorSelections(): void { this.forEachTab((editor, tabIndex, tabContainer, tabLabelWidget, tabLabel, tabActionBar) => { this.redrawTabSelectedActiveAndDirty(this.groupsView.activeGroup === this.groupView, editor, tabContainer, tabActionBar); }); @@ -865,11 +866,11 @@ export class MultiEditorTabsControl extends EditorTabsControl { return this.groupView.getIndexOfEditor(editor); } - private lastSelectedEditor: EditorInput | undefined; + private lastSingleSelectSelectedEditor: EditorInput | undefined; private registerTabListeners(tab: HTMLElement, tabIndex: number, tabsContainer: HTMLElement, tabsScrollbar: ScrollableElement): IDisposable { const disposables = new DisposableStore(); - const handleClickOrTouch = (e: MouseEvent | GestureEvent, preserveFocus: boolean): void => { + const handleClickOrTouch = async (e: MouseEvent | GestureEvent, preserveFocus: boolean): Promise => { tab.blur(); // prevent flicker of focus outline on tab until editor got focus if (isMouseEvent(e) && (e.button !== 0 /* middle/right mouse button */ || (isMacintosh && e.ctrlKey /* macOS context menu */))) { @@ -877,7 +878,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { e.preventDefault(); // required to prevent auto-scrolling (https://github.com/microsoft/vscode/issues/16690) } - return undefined; + return; } if (this.originatesFromTabActionBar(e)) { @@ -889,29 +890,31 @@ export class MultiEditorTabsControl extends EditorTabsControl { if (editor) { if (e.shiftKey) { let anchor; - if (this.lastSelectedEditor && this.groupView.isSelected(this.lastSelectedEditor)) { + if (this.lastSingleSelectSelectedEditor && this.tabsModel.isSelected(this.lastSingleSelectSelectedEditor)) { // The last selected editor is the anchor - anchor = this.lastSelectedEditor; + anchor = this.lastSingleSelectSelectedEditor; } else { // The active editor is the anchor - this.lastSelectedEditor = this.groupView.activeEditor!; + this.lastSingleSelectSelectedEditor = this.groupView.activeEditor!; anchor = this.groupView.activeEditor!; } - this.selectEditorsBetween(editor, anchor); + await this.selectEditorsBetween(editor, anchor); } else if ((e.ctrlKey && !isMacintosh) || (e.metaKey && isMacintosh)) { - if (this.groupView.isSelected(editor)) { - this.groupView.unSelectEditor(editor); + if (this.tabsModel.isSelected(editor)) { + await this.unselectEditor(editor); } else { - this.groupView.selectEditor(editor, true); - this.lastSelectedEditor = editor; + await this.selectEditor(editor); + this.lastSingleSelectSelectedEditor = editor; } } else { // Even if focus is preserved make sure to activate the group. - this.groupView.openEditor(editor, { preserveFocus, activation: EditorActivation.ACTIVATE }, { selected: this.groupView.isSelected(editor) /* Ensures drag and drop does not remove selection */ }); + // If a new active editor is selected, keep the current selection on key + // down such that drag and drop can operate over the selection. The selection + // is removed on key up in this case. + const inactiveSelection = this.tabsModel.isSelected(editor) ? this.groupView.selectedEditors.filter(e => !e.matches(editor)) : []; + await this.groupView.openEditor(editor, { preserveFocus, activation: EditorActivation.ACTIVATE }, { inactiveSelection, focusTabControl: true }); } } - - return undefined; }; const showContextMenu = (e: Event) => { @@ -932,26 +935,23 @@ export class MultiEditorTabsControl extends EditorTabsControl { tabsScrollbar.setScrollPosition({ scrollLeft: tabsScrollbar.getScrollPosition().scrollLeft - e.translationX }); })); - // Prevent flicker of focus outline on tab until editor got focus - disposables.add(addDisposableListener(tab, EventType.MOUSE_UP, e => { + // Update selection & prevent flicker of focus outline on tab until editor got focus + disposables.add(addDisposableListener(tab, EventType.MOUSE_UP, async e => { EventHelper.stop(e); tab.blur(); if (isMouseEvent(e) && (e.button !== 0 /* middle/right mouse button */ || (isMacintosh && e.ctrlKey /* macOS context menu */))) { - if (e.button === 1) { - e.preventDefault(); // required to prevent auto-scrolling (https://github.com/microsoft/vscode/issues/16690) - } - - return undefined; + return; } if (this.originatesFromTabActionBar(e)) { return; // not when clicking on actions } + const isCtrlCmd = (e.ctrlKey && !isMacintosh) || (e.metaKey && isMacintosh); if (!isCtrlCmd && !e.shiftKey && this.groupView.selectedEditors.length > 1) { - this.unselectAllEditors(); + await this.unselectAllEditors(); } })); @@ -1078,23 +1078,14 @@ export class MultiEditorTabsControl extends EditorTabsControl { } isNewWindowOperation = this.isNewWindowOperation(e); - - const draggedEditors = []; const selectedEditors = this.groupView.selectedEditors; - const isMultiSelected = this.groupView.isSelected(editor) && selectedEditors.length > 1; - if (isMultiSelected) { - draggedEditors.push(...selectedEditors); - } else { - draggedEditors.push(editor); - } - - this.editorTransfer.setData(draggedEditors.map(e => new DraggedEditorIdentifier({ editor: e, groupId: this.groupView.id })), DraggedEditorIdentifier.prototype); + this.editorTransfer.setData(selectedEditors.map(e => new DraggedEditorIdentifier({ editor: e, groupId: this.groupView.id })), DraggedEditorIdentifier.prototype); if (e.dataTransfer) { e.dataTransfer.effectAllowed = 'copyMove'; - if (isMultiSelected) { - const label = `${editor.getName()} + ${draggedEditors.length - 1}`; - setupMultiselectDragLabel(label, e.dataTransfer, tab); + if (selectedEditors.length > 1) { + const label = `${editor.getName()} + ${selectedEditors.length - 1}`; + applyDragImage(e, label, 'monaco-editor-group-drag-image', this.getColor(listActiveSelectionBackground), this.getColor(listActiveSelectionForeground)); } else { e.dataTransfer.setDragImage(tab, 0, 0); // top left corner of dragged tab set to cursor position to make room for drop-border feedback } @@ -1163,7 +1154,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { } const targetGroup = auxiliaryEditorPart.activeGroup; - const editors = draggedEditors.map(de => ({ editor: de.identifier.editor, options: {} })); + const editors = draggedEditors.map(de => ({ editor: de.identifier.editor })); if (this.isMoveOperation(lastDragEvent ?? e, targetGroup.id, draggedEditors[0].identifier.editor)) { this.groupView.moveEditors(editors, targetGroup); } else { @@ -1192,7 +1183,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { private isSupportedDropTransfer(e: DragEvent): boolean { if (this.groupTransfer.hasData(DraggedEditorGroupIdentifier.prototype)) { const data = this.groupTransfer.getData(DraggedEditorGroupIdentifier.prototype); - if (Array.isArray(data)) { + if (Array.isArray(data) && data.length > 0) { const group = data[0]; if (group.identifier === this.groupView.id) { return false; // groups cannot be dropped on group it originates from @@ -1282,7 +1273,15 @@ export class MultiEditorTabsControl extends EditorTabsControl { return { leftElement: tabBefore as HTMLElement, rightElement: tabAfter as HTMLElement }; } - private selectEditorsBetween(target: EditorInput, anchor: EditorInput): void { + private async selectEditor(editor: EditorInput): Promise { + if (this.groupView.isActive(editor)) { + return; + } + + await this.groupView.setSelection(editor, this.groupView.selectedEditors); + } + + private async selectEditorsBetween(target: EditorInput, anchor: EditorInput): Promise { const editorIndex = this.groupView.getIndexOfEditor(target); if (editorIndex === -1) { throw new BugIndicatingError(); @@ -1293,29 +1292,71 @@ export class MultiEditorTabsControl extends EditorTabsControl { throw new BugIndicatingError(); } + const selection = this.groupView.selectedEditors; + // Unselect editors on other side of anchor in relation to the target let currentIndex = anchorIndex; while (currentIndex >= 0 && currentIndex <= this.groupView.count - 1) { currentIndex = anchorIndex < editorIndex ? currentIndex - 1 : currentIndex + 1; - const currentEditor = this.groupView.getEditorByIndex(currentIndex); - if (!currentEditor || !this.groupView.isSelected(currentEditor)) { + if (!this.tabsModel.isSelected(currentIndex)) { break; } - this.groupView.unSelectEditor(currentEditor); + const currentEditor = this.groupView.getEditorByIndex(currentIndex); + if (!currentEditor) { + break; + } + + selection.filter(editor => !editor.matches(currentEditor)); } // Select editors between anchor and target const fromIndex = anchorIndex < editorIndex ? anchorIndex : editorIndex; const toIndex = anchorIndex < editorIndex ? editorIndex : anchorIndex; - const selectedEditors = this.groupView.getEditors(EditorsOrder.SEQUENTIAL).slice(fromIndex, toIndex + 1); - this.groupView.selectEditors(selectedEditors, target); + const editorsToSelect = this.groupView.getEditors(EditorsOrder.SEQUENTIAL).slice(fromIndex, toIndex + 1); + for (const editor of editorsToSelect) { + if (!this.tabsModel.isSelected(editor)) { + selection.push(editor); + } + } + + const inactiveSelectedEditors = selection.filter(editor => !editor.matches(target)); + await this.groupView.setSelection(target, inactiveSelectedEditors); } - private unselectAllEditors(): void { - this.groupView.unSelectEditors(this.groupView.selectedEditors.filter(editor => !this.groupView.isActive(editor))); + private async unselectEditor(editor: EditorInput): Promise { + const isUnselectingActiveEditor = this.groupView.isActive(editor); + + // If there is only one editor selected, do not unselect it + if (isUnselectingActiveEditor && this.groupView.selectedEditors.length === 1) { + return; + } + + let newActiveEditor = this.groupView.activeEditor!; + + // If active editor is bing unselected then find the most recently opened selected editor + // that is not the editor being unselected + if (isUnselectingActiveEditor) { + const recentEditors = this.groupView.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE); + for (let i = 1; i < recentEditors.length; i++) { // First one is the active editor + const recentEditor = recentEditors[i]; + if (this.tabsModel.isSelected(recentEditor)) { + newActiveEditor = recentEditor; + break; + } + } + } + + const inactiveSelectedEditors = this.groupView.selectedEditors.filter(e => !e.matches(editor) && !e.matches(newActiveEditor)); + await this.groupView.setSelection(newActiveEditor, inactiveSelectedEditors); + } + + private async unselectAllEditors(): Promise { + if (this.groupView.selectedEditors.length > 1) { + await this.groupView.setSelection(this.groupView.activeEditor!, []); + } } private computeTabLabels(): void { @@ -1604,7 +1645,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { private doRedrawTabActive(isGroupActive: boolean, allowBorderTop: boolean, editor: EditorInput, tabContainer: HTMLElement, tabActionBar: ActionBar): void { const isActive = this.tabsModel.isActive(editor); - const isSelected = this.groupView.isSelected(editor); // TODO, move to model + const isSelected = this.tabsModel.isSelected(editor); tabContainer.classList.toggle('active', isActive); tabContainer.classList.toggle('selected', isSelected); @@ -1620,19 +1661,19 @@ export class MultiEditorTabsControl extends EditorTabsControl { } // Set border TOP if theme defined color - let color: string | null = null; + let tabBorderColorTop: string | null = null; if (allowBorderTop) { if (isActive) { - color = this.getColor(isGroupActive ? TAB_ACTIVE_BORDER_TOP : TAB_UNFOCUSED_ACTIVE_BORDER_TOP); + tabBorderColorTop = this.getColor(isGroupActive ? TAB_ACTIVE_BORDER_TOP : TAB_UNFOCUSED_ACTIVE_BORDER_TOP); } - if (color === null && isSelected) { - color = this.getColor(TAB_SELECTED_BORDER_TOP); + if (tabBorderColorTop === null && isSelected) { + tabBorderColorTop = this.getColor(TAB_SELECTED_BORDER_TOP); } } - tabContainer.classList.toggle('tab-border-top', !!color); - tabContainer.style.setProperty('--tab-border-top-color', color ?? ''); + tabContainer.classList.toggle('tab-border-top', !!tabBorderColorTop); + tabContainer.style.setProperty('--tab-border-top-color', tabBorderColorTop ?? ''); } private doRedrawTabDirty(isGroupActive: boolean, isTabActive: boolean, editor: EditorInput, tabContainer: HTMLElement): boolean { @@ -2158,7 +2199,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { // Check for group transfer if (this.groupTransfer.hasData(DraggedEditorGroupIdentifier.prototype)) { const data = this.groupTransfer.getData(DraggedEditorGroupIdentifier.prototype); - if (Array.isArray(data)) { + if (Array.isArray(data) && data.length > 0) { const sourceGroup = this.editorPartsView.getGroup(data[0].identifier); if (sourceGroup) { const mergeGroupOptions: IMergeGroupOptions = { index: targetEditorIndex }; @@ -2177,18 +2218,21 @@ export class MultiEditorTabsControl extends EditorTabsControl { // Check for editor transfer else if (this.editorTransfer.hasData(DraggedEditorIdentifier.prototype)) { const data = this.editorTransfer.getData(DraggedEditorIdentifier.prototype); - if (Array.isArray(data)) { + if (Array.isArray(data) && data.length > 0) { + const sourceGroup = data.length ? this.editorPartsView.getGroup(data[0].identifier.groupId) : undefined; + const isLocalMove = sourceGroup === this.groupView; // Keep the same order when moving / copying editors within the same group for (const de of data) { const editor = de.identifier.editor; - const sourceGroup = this.editorPartsView.getGroup(de.identifier.groupId); - if (!sourceGroup) { + + // Only allow moving/copying from a single group source + if (!sourceGroup || sourceGroup.id !== de.identifier.groupId) { continue; } const sourceEditorIndex = sourceGroup.getIndexOfEditor(editor); - if (sourceGroup === this.groupView && sourceEditorIndex < targetEditorIndex) { + if (isLocalMove && sourceEditorIndex < targetEditorIndex) { targetEditorIndex--; } @@ -2209,7 +2253,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { // Check for tree items else if (this.treeItemsTransfer.hasData(DraggedTreeItemsIdentifier.prototype)) { const data = this.treeItemsTransfer.getData(DraggedTreeItemsIdentifier.prototype); - if (Array.isArray(data)) { + if (Array.isArray(data) && data.length > 0) { const editors: IUntypedEditorInput[] = []; for (const id of data) { const dataTransferItem = await this.treeViewsDragAndDropService.removeDragOperationTransfer(id.identifier); @@ -2239,22 +2283,6 @@ export class MultiEditorTabsControl extends EditorTabsControl { } } -function setupMultiselectDragLabel(text: string, dataTransfer: DataTransfer, tab: HTMLElement) { - const dragImage = $('.monaco-drag-image'); - dragImage.textContent = text; - const getDragImageContainer = (e: HTMLElement | null) => { - while (e && !e.classList.contains('monaco-workbench')) { - e = e.parentElement; - } - return e || getActiveDocument().body; - }; - - const container = getDragImageContainer(tab); - container.appendChild(dragImage); - dataTransfer.setDragImage(dragImage, -10, -10); - setTimeout(() => container.removeChild(dragImage), 0); -} - registerThemingParticipant((theme, collector) => { // Add bottom border to tabs when wrapping diff --git a/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts index 50683f16fe0..908b7d85cfb 100644 --- a/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts @@ -163,12 +163,9 @@ export class MultiRowEditorControl extends Disposable implements IEditorTabsCont this.unstickyEditorTabsControl.setActive(isActive); } - setEditorSelections(editors: EditorInput[], selected: boolean): void { - const stickyEditors = editors.filter(e => this.model.isSticky(e)); - const unstickyEditors = editors.filter(e => !this.model.isSticky(e)); - - this.stickyEditorTabsControl.setEditorSelections(stickyEditors, selected); - this.unstickyEditorTabsControl.setEditorSelections(unstickyEditors, selected); + updateEditorSelections(): void { + this.stickyEditorTabsControl.updateEditorSelections(); + this.unstickyEditorTabsControl.updateEditorSelections(); } updateEditorLabel(editor: EditorInput): void { diff --git a/src/vs/workbench/browser/parts/editor/noEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/noEditorTabsControl.ts index 36a8ac0f0c2..6a0859cd3ec 100644 --- a/src/vs/workbench/browser/parts/editor/noEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/noEditorTabsControl.ts @@ -61,7 +61,7 @@ export class NoEditorTabsControl extends EditorTabsControl { setActive(isActive: boolean): void { } - setEditorSelections(editors: EditorInput[], selected: boolean): void { } + updateEditorSelections(): void { } updateEditorLabel(editor: EditorInput): void { } diff --git a/src/vs/workbench/browser/parts/editor/singleEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/singleEditorTabsControl.ts index 4b4e48163df..b6b3dd436ac 100644 --- a/src/vs/workbench/browser/parts/editor/singleEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/singleEditorTabsControl.ts @@ -187,7 +187,7 @@ export class SingleEditorTabsControl extends EditorTabsControl { this.redraw(); } - setEditorSelections(editors: EditorInput[], selected: boolean): void { } + updateEditorSelections(): void { } updateEditorLabel(editor: EditorInput): void { this.ifEditorIsActive(editor, () => this.redraw()); diff --git a/src/vs/workbench/common/contextkeys.ts b/src/vs/workbench/common/contextkeys.ts index 63780b3830e..97937218bbb 100644 --- a/src/vs/workbench/common/contextkeys.ts +++ b/src/vs/workbench/common/contextkeys.ts @@ -72,8 +72,8 @@ export const ActiveEditorGroupLastContext = new RawContextKey('activeEd export const ActiveEditorGroupLockedContext = new RawContextKey('activeEditorGroupLocked', false, localize('activeEditorGroupLocked', "Whether the active editor group is locked")); export const MultipleEditorGroupsContext = new RawContextKey('multipleEditorGroups', false, localize('multipleEditorGroups', "Whether there are multiple editor groups opened")); export const SingleEditorGroupsContext = MultipleEditorGroupsContext.toNegated(); -export const MultipleEditorsSelectedContext = new RawContextKey('multipleEditorsSelected', false, localize('multipleEditorsSelected', "Whether multiple editors have been selected")); -export const TwoEditorsSelectedContext = new RawContextKey('twoEditorsSelected', false, localize('twoEditorsSelected', "Whether two editors have been selected")); +export const MultipleEditorsSelectedInGroupContext = new RawContextKey('multipleEditorsSelectedInGroup', false, localize('multipleEditorsSelectedInGroup', "Whether multiple editors have been selected in an editor group")); +export const TwoEditorsSelectedInGroupContext = new RawContextKey('twoEditorsSelectedInGroup', false, localize('twoEditorsSelectedInGroup', "Whether exactly two editors have been selected in an editor group")); // Editor Part Context Keys export const EditorPartMultipleEditorGroupsContext = new RawContextKey('editorPartMultipleEditorGroups', false, localize('editorPartMultipleEditorGroups', "Whether there are multiple editor groups opened in an editor part")); diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index 91af41524fe..02aff158f4f 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -1168,6 +1168,9 @@ export const enum GroupModelChangeKind { GROUP_LABEL, GROUP_LOCKED, + /* Editors Change */ + EDITORS_SELECTION, + /* Editor Changes */ EDITOR_OPEN, EDITOR_CLOSE, @@ -1177,7 +1180,6 @@ export const enum GroupModelChangeKind { EDITOR_CAPABILITIES, EDITOR_PIN, EDITOR_TRANSIENT, - EDITOR_SELECTION, EDITOR_STICKY, EDITOR_DIRTY, EDITOR_WILL_DISPOSE diff --git a/src/vs/workbench/common/editor/editorGroupModel.ts b/src/vs/workbench/common/editor/editorGroupModel.ts index 146224c838a..d8b7d956adc 100644 --- a/src/vs/workbench/common/editor/editorGroupModel.ts +++ b/src/vs/workbench/common/editor/editorGroupModel.ts @@ -25,7 +25,7 @@ export interface IEditorOpenOptions { readonly sticky?: boolean; readonly transient?: boolean; active?: boolean; - readonly selected?: boolean; + readonly inactiveSelection?: EditorInput[]; readonly index?: number; readonly supportSideBySide?: SideBySideEditor.ANY | SideBySideEditor.BOTH; } @@ -196,8 +196,7 @@ interface IEditorGroupModel extends IReadonlyEditorGroupModel { closeEditor(editor: EditorInput, context?: EditorCloseContext, openNext?: boolean): IEditorCloseResult | undefined; moveEditor(editor: EditorInput, toIndex: number): EditorInput | undefined; setActive(editor: EditorInput | undefined): EditorInput | undefined; - selectEditor(editor: EditorInput, active?: boolean): EditorInput | undefined; - unselectEditor(editor: EditorInput): EditorInput | undefined; + setSelection(activeSelectedEditor: EditorInput, inactiveSelectedEditors: EditorInput[]): void; } export class EditorGroupModel extends Disposable implements IEditorGroupModel { @@ -221,13 +220,13 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { private locked = false; - private selected: EditorInput[] = []; // editors in selected state, first one is active + private selection: EditorInput[] = []; // editors in selected state, first one is active private set active(editor: EditorInput | null) { - this.selected = editor ? [editor] : []; + this.selection = editor ? [editor] : []; } private get active(): EditorInput | null { - return this.selected[0] ?? null; + return this.selection[0] ?? null; } private preview: EditorInput | null = null; // editor in preview state @@ -414,10 +413,8 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { }; this._onDidModelChange.fire(event); - // Handle active - if (makeActive) { - this.doSetActive(newEditor, targetIndex, options?.selected); - } + // Handle active & selection + this.setSelection(makeActive ? newEditor : this.activeEditor, options?.inactiveSelection ?? []); return { editor: newEditor, @@ -437,10 +434,8 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { this.doPin(existingEditor, existingEditorIndex); } - // Activate it - if (makeActive) { - this.doSetActive(existingEditor, existingEditorIndex, options?.selected); - } + // Activate / select it + this.setSelection(makeActive ? existingEditor : this.activeEditor, options?.inactiveSelection ?? []); // Respect index if (options && typeof options.index === 'number') { @@ -575,7 +570,8 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { } } - this.doSetActive(newActive, this.editors.indexOf(newActive)); + const newSelection = this.selection.filter(selected => !selected.matches(newActive) && !selected.matches(editor)); + this.doSetSelection(newActive, this.editors.indexOf(newActive), newSelection); } // One Editor @@ -586,9 +582,10 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { // Remove from selection else if (!isActiveEditor) { - const wasSelected = !!this.selected.find(selected => this.matches(selected, editor)); + const wasSelected = !!this.selection.find(selected => this.matches(selected, editor)); if (wasSelected) { - this.doSetSelected(editor, index, false); + const newSelection = this.selection.filter(selected => !selected.matches(editor)); + this.doSetSelection(this.activeEditor!, this.indexOf(this.activeEditor), newSelection); } } @@ -693,17 +690,13 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { return editor; } - private doSetActive(editor: EditorInput, editorIndex: number, selected: boolean = false): void { + private doSetActive(editor: EditorInput, editorIndex: number): void { if (this.matches(this.active, editor)) { + this.selection = [editor]; return; // already active } - if (selected) { - this.selected = this.selected.filter(selected => selected !== editor); - this.selected.unshift(editor); - } else { - this.selected = [editor]; - } + this.active = editor; // Bring to front in MRU list const mruIndex = this.indexOf(editor, this.mru); @@ -717,13 +710,6 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { editorIndex }; this._onDidModelChange.fire(event); - - const selectionEvent: IGroupEditorChangeEvent = { - kind: GroupModelChangeKind.EDITOR_SELECTION, - editor, - editorIndex - }; - this._onDidModelChange.fire(selectionEvent); } public get selectedEditors(): EditorInput[] { @@ -736,64 +722,33 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { editor = this.editors[editor]; } - return !!this.selected.find(selectedEditor => this.matches(selectedEditor, editor)); + return !!this.selection.find(selectedEditor => this.matches(selectedEditor, editor)); } - selectEditor(candidate: EditorInput, active: boolean = false): EditorInput | undefined { - const res = this.findEditor(candidate); + setSelection(activeSelectedEditor: EditorInput, inactiveSelectedEditors: EditorInput[]): void { + const res = this.findEditor(activeSelectedEditor); if (!res) { - return; // not found + return; } - const [editor, editorIndex] = res; + const [newActiveEditor, newActiveEditorIndex] = res; - this.doSetSelected(editor, editorIndex, true, active); - - return editor; + this.doSetSelection(newActiveEditor, newActiveEditorIndex, inactiveSelectedEditors); } - unselectEditor(candidate: EditorInput): EditorInput | undefined { - const res = this.findEditor(candidate); - if (!res) { - return; // not found - } + private doSetSelection(newActiveEditor: EditorInput, activeEditorIndex: number, inactiveSelectedEditors: EditorInput[]): void { + this.doSetActive(newActiveEditor, activeEditorIndex); - const [editor, editorIndex] = res; - - this.doSetSelected(editor, editorIndex, false); - - return editor; - } - - private doSetSelected(editor: EditorInput, editorIndex: number, select: boolean, active: boolean = false): void { - if (select) { - if (this.isSelected(editor)) { - return; + for (const candidate of inactiveSelectedEditors) { + const editor = this.findEditor(candidate)?.[0]; + if (editor && !this.isSelected(editor)) { + this.selection.push(editor); } - - if (active) { - this.doSetActive(editor, editorIndex, true); - } else { - this.selected.push(editor); - } - } else { - if (!this.isSelected(editor)) { - return; - } - - if (this.matches(this.active, editor)) { - console.warn('Cannot unselect the active editor'); - return; - } - - this.selected.splice(this.selected.indexOf(editor), 1); } // Event - const event: IGroupEditorChangeEvent = { - kind: GroupModelChangeKind.EDITOR_SELECTION, - editor, - editorIndex + const event: IGroupModelChangeEvent = { + kind: GroupModelChangeKind.EDITORS_SELECTION, }; this._onDidModelChange.fire(event); } diff --git a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts index 663156554b5..963e5c56925 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts @@ -20,7 +20,7 @@ import { CLOSE_SAVED_EDITORS_COMMAND_ID, CLOSE_EDITORS_IN_GROUP_COMMAND_ID, CLOS import { AutoSaveAfterShortDelayContext } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { WorkbenchListDoubleSelection } from 'vs/platform/list/browser/listService'; import { Schemas } from 'vs/base/common/network'; -import { DirtyWorkingCopiesContext, EnterMultiRootWorkspaceSupportContext, HasWebFileSystemAccess, WorkbenchStateContext, WorkspaceFolderCountContext, SidebarFocusContext, ActiveEditorCanRevertContext, ActiveEditorContext, ResourceContextKey, ActiveEditorAvailableEditorIdsContext, MultipleEditorsSelectedContext, TwoEditorsSelectedContext } from 'vs/workbench/common/contextkeys'; +import { DirtyWorkingCopiesContext, EnterMultiRootWorkspaceSupportContext, HasWebFileSystemAccess, WorkbenchStateContext, WorkspaceFolderCountContext, SidebarFocusContext, ActiveEditorCanRevertContext, ActiveEditorContext, ResourceContextKey, ActiveEditorAvailableEditorIdsContext, MultipleEditorsSelectedInGroupContext, TwoEditorsSelectedInGroupContext } from 'vs/workbench/common/contextkeys'; import { IsWebContext } from 'vs/platform/contextkey/common/contextkeys'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ThemeIcon } from 'vs/base/common/themables'; @@ -160,7 +160,7 @@ appendEditorTitleContextMenuItem(COPY_RELATIVE_PATH_COMMAND_ID, copyRelativePath appendEditorTitleContextMenuItem(REVEAL_IN_EXPLORER_COMMAND_ID, nls.localize('revealInSideBar', "Reveal in Explorer View"), ResourceContextKey.IsFileSystemResource, '2_files', false, 1); export function appendEditorTitleContextMenuItem(id: string, title: string, when: ContextKeyExpression | undefined, group: string, supportsMultiSelect: boolean, order?: number): void { - const precondition = supportsMultiSelect !== true ? MultipleEditorsSelectedContext.negate() : undefined; + const precondition = supportsMultiSelect !== true ? MultipleEditorsSelectedInGroupContext.negate() : undefined; // Menu MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { @@ -420,7 +420,7 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { group: '3_compare', order: 30, command: compareSelectedCommand, - when: ContextKeyExpr.and(ResourceContextKey.HasResource, TwoEditorsSelectedContext, isFileOrUntitledResourceContextKey) + when: ContextKeyExpr.and(ResourceContextKey.HasResource, TwoEditorsSelectedInGroupContext, isFileOrUntitledResourceContextKey) }); MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { diff --git a/src/vs/workbench/contrib/files/browser/files.ts b/src/vs/workbench/contrib/files/browser/files.ts index 7fc74195991..f1dbd239f4e 100644 --- a/src/vs/workbench/contrib/files/browser/files.ts +++ b/src/vs/workbench/contrib/files/browser/files.ts @@ -137,12 +137,14 @@ export function getMultiSelectedResources(resource: URI | object | undefined, li } } + // Check for tabs multiselect. const activeGroup = editorGroupService.activeGroup; const selection = activeGroup.selectedEditors; - if (selection.length) { - const selectedResources = selection.map(editor => EditorResourceAccessor.getOriginalUri(editor)).filter(uri => !!uri) as URI[]; - if (selectedResources.some(r => r.toString() === resource?.toString())) { - return selectedResources; + if (selection.length > 1 && URI.isUri(resource)) { + // If the resource is part of the tabs selection, return all selected tabs/resources. + // It's possible that multiple tabs are selected but the action was applied to a resource that is not part of the selection. + if (selection.some(e => e.matches({ resource }))) { + return selection.map(editor => EditorResourceAccessor.getOriginalUri(editor)).filter(uri => !!uri); } } diff --git a/src/vs/workbench/services/editor/common/editorGroupsService.ts b/src/vs/workbench/services/editor/common/editorGroupsService.ts index 6ecc2b3d333..8670f77ece2 100644 --- a/src/vs/workbench/services/editor/common/editorGroupsService.ts +++ b/src/vs/workbench/services/editor/common/editorGroupsService.ts @@ -773,33 +773,20 @@ export interface IEditorGroup { */ isActive(editor: EditorInput | IUntypedEditorInput): boolean; - /** - * Selects the editor in the group. If active is set to true, - * it will be the active editor in the group. - */ - selectEditor(editor: EditorInput, active?: boolean): Promise; - - /** - * Selects the editors in the group. If activeEditor is provided, - * it will be the active editor in the group. - */ - selectEditors(editors: EditorInput[], activeEditor?: EditorInput): Promise; - - /** - * Unselects the editor in the group. If the editor is not specified, unselects the active editor. - */ - unSelectEditor(editor: EditorInput): Promise; - - /** - * Unselects the editors in the group. If the editor is not specified, unselects the active editor. - */ - unSelectEditors(editors: EditorInput[]): Promise; - /** * Whether the editor is selected in the group. */ isSelected(editor: EditorInput): boolean; + /** + * Set a new selection for this group. This will replace the current + * selection with the new selection. + * + * @param activeSelectedEditor the editor to set as active selected editor + * @param inactiveSelectedEditors the inactive editors to set as selected + */ + setSelection(activeSelectedEditor: EditorInput, inactiveSelectedEditors: EditorInput[]): Promise; + /** * Find out if a certain editor is included in the group. * diff --git a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts index 56c29a69a4f..f458db5c34f 100644 --- a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts @@ -1538,7 +1538,7 @@ suite('EditorGroupsService', () => { assert.strictEqual(group.getIndexOfEditor(inputSticky), 0); }); - test('selection: select/unselect, isSelected/getSelectedEditors', async () => { + test('selection: setSelection, isSelected, selectedEditors', async () => { const [part] = await createPart(); const group = part.activeGroup; @@ -1555,9 +1555,9 @@ suite('EditorGroupsService', () => { return inputs.length === group.selectedEditors.length; } + // Active: input1, Selected: input1 await group.openEditors([input1, input2, input3].map(editor => ({ editor, options: { pinned: true } }))); - // Active: input1, Selected: input1 assert.strictEqual(group.isActive(input1), true); assert.strictEqual(group.isSelected(input1), true); assert.strictEqual(group.isSelected(input2), false); @@ -1565,9 +1565,9 @@ suite('EditorGroupsService', () => { assert.strictEqual(isSelection([input1]), true); - await group.selectEditor(input3); - // Active: input1, Selected: input1, input3 + await group.setSelection(input1, [input3]); + assert.strictEqual(group.isActive(input1), true); assert.strictEqual(group.isSelected(input1), true); assert.strictEqual(group.isSelected(input2), false); @@ -1575,9 +1575,9 @@ suite('EditorGroupsService', () => { assert.strictEqual(isSelection([input1, input3]), true); - await group.selectEditor(input2, true); - // Active: input2, Selected: input1, input3 + await group.setSelection(input2, [input1, input3]); + assert.strictEqual(group.isSelected(input1), true); assert.strictEqual(group.isActive(input2), true); assert.strictEqual(group.isSelected(input2), true); @@ -1585,24 +1585,15 @@ suite('EditorGroupsService', () => { assert.strictEqual(isSelection([input1, input2, input3]), true); - await group.unSelectEditor(input2); + await group.setSelection(input1, []); // Selected: input3 assert.strictEqual(group.isActive(input1), true); assert.strictEqual(group.isSelected(input1), true); assert.strictEqual(group.isSelected(input2), false); - assert.strictEqual(group.isSelected(input3), true); + assert.strictEqual(group.isSelected(input3), false); - assert.strictEqual(isSelection([input1, input3]), true); - - await group.unSelectEditors([input1]); - - // Selected: NONE - assert.strictEqual(group.isSelected(input1), false); - assert.strictEqual(group.isSelected(input2), false); - assert.strictEqual(group.isSelected(input3), true); - - assert.strictEqual(isSelection([input3]), true); + assert.strictEqual(isSelection([input1]), true); }); test('moveEditor with context (across groups)', async () => { diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index f6edde59ddb..e8c445facde 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -928,10 +928,7 @@ export class TestEditorGroupView implements IEditorGroupView { isSticky(_editor: EditorInput): boolean { return false; } isTransient(_editor: EditorInput): boolean { return false; } isActive(_editor: EditorInput | IUntypedEditorInput): boolean { return false; } - selectEditor(_editor: EditorInput, _active?: boolean): Promise { throw new Error('not implemented'); } - selectEditors(_editors: EditorInput[], _activeEditor?: EditorInput): Promise { throw new Error('not implemented'); } - unSelectEditor(_editor: EditorInput): Promise { throw new Error('not implemented'); } - unSelectEditors(_editors: EditorInput[]): Promise { throw new Error('not implemented'); } + setSelection(_activeSelectedEditor: EditorInput, _inactiveSelectedEditors: EditorInput[]): Promise { throw new Error('not implemented'); } isSelected(_editor: EditorInput): boolean { return false; } contains(candidate: EditorInput | IUntypedEditorInput): boolean { return false; } moveEditor(_editor: EditorInput, _target: IEditorGroup, _options?: IEditorOptions): boolean { return true; } From dd05a6ff19bc6332ba63f5dbf9d02bde1dac5df9 Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 21 May 2024 17:41:25 +0200 Subject: [PATCH 292/357] remove bridge agents and agent providers because inline chat is all participants now * remove much of IInlineChatSessionProvider * deprecate IInlineChatService and move most consumers off --- .../emptyTextEditorHint.ts | 22 +- .../browser/inlineChat.contribution.ts | 9 +- .../browser/inlineChatController.ts | 35 +- .../inlineChat/browser/inlineChatSession.ts | 7 +- .../browser/inlineChatSessionServiceImpl.ts | 563 +++--------------- .../contrib/inlineChat/common/inlineChat.ts | 34 +- .../test/browser/inlineChatSession.test.ts | 27 +- .../cellDiagnosticEditorContrib.ts | 11 +- .../contrib/editorHint/emptyCellEditorHint.ts | 6 +- .../test/browser/terminalInitialHint.test.ts | 23 +- 10 files changed, 145 insertions(+), 592 deletions(-) diff --git a/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts b/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts index fab89ecfe3c..b047830a881 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts @@ -22,7 +22,6 @@ import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editor import { IContentActionHandler, renderFormattedText } from 'vs/base/browser/formattedTextRenderer'; import { ApplyFileSnippetAction } from 'vs/workbench/contrib/snippets/browser/commands/fileTemplateSnippets'; import { IInlineChatSessionService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionService'; -import { IInlineChatService, IInlineChatSessionProvider } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; import { IProductService } from 'vs/platform/product/common/productService'; @@ -36,6 +35,7 @@ import { LOG_MODE_ID, OUTPUT_MODE_ID } from 'vs/workbench/services/output/common import { SEARCH_RESULT_LANGUAGE_ID } from 'vs/workbench/services/search/common/search'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { IHoverService } from 'vs/platform/hover/browser/hover'; +import { ChatAgentLocation, IChatAgent, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; const $ = dom.$; @@ -76,7 +76,7 @@ export class EmptyTextEditorHintContribution implements IEditorContribution { @IHoverService protected readonly hoverService: IHoverService, @IKeybindingService private readonly keybindingService: IKeybindingService, @IInlineChatSessionService private readonly inlineChatSessionService: IInlineChatSessionService, - @IInlineChatService protected readonly inlineChatService: IInlineChatService, + @IChatAgentService private readonly chatAgentService: IChatAgentService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IProductService protected readonly productService: IProductService, ) { @@ -84,7 +84,7 @@ export class EmptyTextEditorHintContribution implements IEditorContribution { this.toDispose.push(this.editor.onDidChangeModel(() => this.update())); this.toDispose.push(this.editor.onDidChangeModelLanguage(() => this.update())); this.toDispose.push(this.editor.onDidChangeModelContent(() => this.update())); - this.toDispose.push(this.inlineChatService.onDidChangeProviders(() => this.update())); + this.toDispose.push(this.chatAgentService.onDidChangeAgents(() => this.update())); this.toDispose.push(this.editor.onDidChangeModelDecorations(() => this.update())); this.toDispose.push(this.editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => { if (e.hasChanged(EditorOption.readOnly)) { @@ -146,9 +146,9 @@ export class EmptyTextEditorHintContribution implements IEditorContribution { return false; } - const inlineChatProviders = [...this.inlineChatService.getAllProvider()]; - const shouldRenderDefaultHint = model?.uri.scheme === Schemas.untitled && languageId === PLAINTEXT_LANGUAGE_ID && !inlineChatProviders.length; - return inlineChatProviders.length > 0 || shouldRenderDefaultHint; + const hasEditorAgents = Boolean(this.chatAgentService.getDefaultAgent(ChatAgentLocation.Editor)); + const shouldRenderDefaultHint = model?.uri.scheme === Schemas.untitled && languageId === PLAINTEXT_LANGUAGE_ID && hasEditorAgents; + return hasEditorAgents || shouldRenderDefaultHint; } protected update(): void { @@ -162,7 +162,7 @@ export class EmptyTextEditorHintContribution implements IEditorContribution { this.configurationService, this.hoverService, this.keybindingService, - this.inlineChatService, + this.chatAgentService, this.telemetryService, this.productService ); @@ -195,7 +195,7 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { private readonly configurationService: IConfigurationService, private readonly hoverService: IHoverService, private readonly keybindingService: IKeybindingService, - private readonly inlineChatService: IInlineChatService, + private readonly chatAgentService: IChatAgentService, private readonly telemetryService: ITelemetryService, private readonly productService: IProductService ) { @@ -218,8 +218,8 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { return EmptyTextEditorHintContentWidget.ID; } - private _getHintInlineChat(providers: IInlineChatSessionProvider[]) { - const providerName = (providers.length === 1 ? providers[0].label : undefined) ?? this.productService.nameShort; + private _getHintInlineChat(providers: IChatAgent[]) { + const providerName = (providers.length === 1 ? providers[0].fullName : undefined) ?? this.productService.nameShort; const inlineChatId = 'inlineChat.start'; let ariaLabel = `Ask ${providerName} something or start typing to dismiss.`; @@ -399,7 +399,7 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { this.domNode.style.width = 'max-content'; this.domNode.style.paddingLeft = '4px'; - const inlineChatProviders = [...this.inlineChatService.getAllProvider()]; + const inlineChatProviders = this.chatAgentService.getActivatedAgents().filter(candidate => candidate.locations.includes(ChatAgentLocation.Editor)); const { hintElement, ariaLabel } = !inlineChatProviders.length ? this._getHintDefault() : this._getHintInlineChat(inlineChatProviders); this.domNode.append(hintElement); this.ariaLabel = ariaLabel.concat(localize('disableHint', ' Toggle {0} in settings to disable this hint.', AccessibilityVerbositySettingId.EmptyEditorHint)); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts index 2044d45b2e3..2edb4d039c5 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts @@ -13,24 +13,23 @@ import { InlineChatServiceImpl } from 'vs/workbench/contrib/inlineChat/common/in import { Registry } from 'vs/platform/registry/common/platform'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { InlineChatNotebookContribution } from 'vs/workbench/contrib/inlineChat/browser/inlineChatNotebook'; -import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { IWorkbenchContributionsRegistry, registerWorkbenchContribution2, Extensions as WorkbenchExtensions, WorkbenchPhase } from 'vs/workbench/common/contributions'; import { InlineChatSavingServiceImpl } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSavingServiceImpl'; import { InlineChatAccessibleView } from 'vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView'; import { IInlineChatSavingService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSavingService'; import { IInlineChatSessionService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionService'; -import { InlineChatSessionServiceImpl } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl'; +import { InlineChatEnabler, InlineChatSessionServiceImpl } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl'; import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; // --- browser registerSingleton(IInlineChatService, InlineChatServiceImpl, InstantiationType.Delayed); -registerSingleton(IInlineChatSessionService, InlineChatSessionServiceImpl, InstantiationType.Eager); // EAGER because this registers an agent which we need swiftly +registerSingleton(IInlineChatSessionService, InlineChatSessionServiceImpl, InstantiationType.Delayed); registerSingleton(IInlineChatSavingService, InlineChatSavingServiceImpl, InstantiationType.Delayed); registerEditorContribution(INLINE_CHAT_ID, InlineChatController, EditorContributionInstantiation.Eager); // EAGER because of notebook dispose/create of editors -AccessibleViewRegistry.register(new InlineChatAccessibleView()); registerAction2(InlineChatActions.StartSessionAction); registerAction2(InlineChatActions.CloseAction); @@ -58,4 +57,6 @@ registerAction2(InlineChatActions.CopyRecordings); const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchContributionsRegistry.registerWorkbenchContribution(InlineChatNotebookContribution, LifecyclePhase.Restored); +registerWorkbenchContribution2(InlineChatEnabler.Id, InlineChatEnabler, WorkbenchPhase.AfterRestored); + AccessibleViewRegistry.register(new InlineChatAccessibleView()); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 1a1048c1db5..bac8e7419f3 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as aria from 'vs/base/browser/ui/aria/aria'; -import { Barrier, DeferredPromise, Queue, raceCancellation } from 'vs/base/common/async'; +import { Barrier, DeferredPromise, Queue } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { onUnexpectedError } from 'vs/base/common/errors'; @@ -38,7 +38,6 @@ import { IInlineChatSessionService } from './inlineChatSessionService'; import { EditModeStrategy, IEditObserver, LiveStrategy, PreviewStrategy, ProgressingEditsOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatStrategies'; import { InlineChatZoneWidget } from './inlineChatZoneWidget'; import { CTX_INLINE_CHAT_DID_EDIT, CTX_INLINE_CHAT_LAST_FEEDBACK, CTX_INLINE_CHAT_RESPONSE_TYPES, CTX_INLINE_CHAT_SUPPORT_ISSUE_REPORTING, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_VISIBLE, EditMode, INLINE_CHAT_ID, InlineChatConfigKeys, InlineChatResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; -import { ICommandService } from 'vs/platform/commands/common/commands'; import { StashedSession } from './inlineChatSession'; import { IModelDeltaDecoration, ITextModel, IValidEditOperation } from 'vs/editor/common/model'; import { InlineChatContentWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget'; @@ -147,7 +146,6 @@ export class InlineChatController implements IEditorContribution { @IContextKeyService contextKeyService: IContextKeyService, @IChatAgentService private readonly _chatAgentService: IChatAgentService, @IChatService private readonly _chatService: IChatService, - @ICommandService private readonly _commandService: ICommandService, @ILanguageFeaturesService private readonly _languageFeatureService: ILanguageFeaturesService, @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, ) { @@ -442,7 +440,7 @@ export class InlineChatController implements IEditorContribution { })); // Update context key - this._ctxSupportIssueReporting.set(this._session.provider.supportIssueReporting ?? false); + this._ctxSupportIssueReporting.set(this._session.agent.metadata.supportIssueReporting ?? false); // #region DEBT // DEBT@jrieken @@ -616,7 +614,7 @@ export class InlineChatController implements IEditorContribution { return false; }); if (refer && slashCommandLike && !this._session.lastExchange) { - this._log('[IE] seeing refer command, continuing outside editor', this._session.provider.extensionId); + this._log('[IE] seeing refer command, continuing outside editor', this._session.agent.extensionId); // cancel this request this._chatService.cancelCurrentRequestForSession(request.session.sessionId); @@ -811,31 +809,6 @@ export class InlineChatController implements IEditorContribution { this._zone.value.widget.updateToolbar(true); newPosition = await this._strategy.renderChanges(response); - - if (this._session.provider.provideFollowups) { - const followupCts = new CancellationTokenSource(); - const msgListener = Event.once(this._messages.event)(() => { - followupCts.cancel(); - }); - const followupTask = this._session.provider.provideFollowups(this._session.session, response.raw, followupCts.token); - this._log('followup request started', this._session.provider.extensionId, this._session.session, response.raw); - raceCancellation(Promise.resolve(followupTask), followupCts.token).then(followupReply => { - if (followupReply && this._session) { - this._log('followup request received', this._session.provider.extensionId, this._session.session, followupReply); - this._zone.value.widget.updateFollowUps(followupReply, followup => { - if (followup.kind === 'reply') { - this.updateInput(followup.message); - this.acceptInput(); - } else { - this._commandService.executeCommand(followup.commandId, ...(followup.args ?? [])); - } - }); - } - }).finally(() => { - msgListener.dispose(); - followupCts.dispose(); - }); - } } this._showWidget(false, newPosition); @@ -982,7 +955,7 @@ export class InlineChatController implements IEditorContribution { assertType(this._strategy); const moreMinimalEdits = await this._editorWorkerService.computeMoreMinimalEdits(this._session.textModelN.uri, edits); - this._log('edits from PROVIDER and after making them MORE MINIMAL', this._session.provider.extensionId, edits, moreMinimalEdits); + this._log('edits from PROVIDER and after making them MORE MINIMAL', this._session.agent.extensionId, edits, moreMinimalEdits); if (moreMinimalEdits?.length === 0) { // nothing left to do diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts index 6a6d05d7454..2bebeaede34 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts @@ -8,7 +8,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { ResourceEdit, ResourceFileEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; import { IWorkspaceTextEdit, TextEdit, WorkspaceEdit } from 'vs/editor/common/languages'; import { IIdentifiedSingleEditOperation, IModelDecorationOptions, IModelDeltaDecoration, ITextModel, IValidEditOperation, TrackedRangeStickiness } from 'vs/editor/common/model'; -import { EditMode, IInlineChatSessionProvider, IInlineChatSession, IInlineChatBulkEditResponse, IInlineChatEditResponse, InlineChatResponseType, CTX_INLINE_CHAT_HAS_STASHED_SESSION } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { EditMode, IInlineChatSession, IInlineChatBulkEditResponse, IInlineChatEditResponse, InlineChatResponseType, CTX_INLINE_CHAT_HAS_STASHED_SESSION } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { IRange, Range } from 'vs/editor/common/core/range'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { toErrorMessage } from 'vs/base/common/errorMessage'; @@ -34,6 +34,7 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/c import { ILogService } from 'vs/platform/log/common/log'; import { ChatModel, IChatRequestModel, IChatResponseModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { IChatAgent } from 'vs/workbench/contrib/chat/common/chatAgents'; export type TelemetryData = { @@ -160,7 +161,7 @@ export class Session { * The document into which AI edits went, when live this is `targetUri` otherwise it is a temporary document */ readonly textModelN: ITextModel, - readonly provider: IInlineChatSessionProvider, + readonly agent: IChatAgent, readonly session: IInlineChatSession, readonly wholeRange: SessionWholeRange, readonly hunkData: HunkData, @@ -168,7 +169,7 @@ export class Session { ) { this.textModelNAltVersion = textModelN.getAlternativeVersionId(); this._teldata = { - extension: ExtensionIdentifier.toKey(provider.extensionId), + extension: ExtensionIdentifier.toKey(agent.extensionId), startTime: this._startTime.toISOString(), endTime: this._startTime.toISOString(), edits: 0, diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts index 5f1e3ac6b3d..7e77096a55c 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts @@ -2,216 +2,36 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { coalesceInPlace, isNonEmptyArray } from 'vs/base/common/arrays'; -import { raceCancellation } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { CancellationError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { MarkdownString } from 'vs/base/common/htmlContent'; -import { Iterable } from 'vs/base/common/iterator'; -import { DisposableMap, DisposableStore, IDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { LRUCache } from 'vs/base/common/map'; +import { DisposableStore, IDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { IActiveCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IRange, Range } from 'vs/editor/common/core/range'; -import { TextEdit, WorkspaceEdit } from 'vs/editor/common/languages'; -import { ITextModel, IValidEditOperation } from 'vs/editor/common/model'; +import { IValidEditOperation } from 'vs/editor/common/model'; import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { IModelService } from 'vs/editor/common/services/model'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; -import { IProgress, Progress } from 'vs/platform/progress/common/progress'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { DEFAULT_EDITOR_ASSOCIATION } from 'vs/workbench/common/editor'; -import { ChatAgentLocation, IChatAgent, IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentImplementation, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { IChatFollowup, IChatProgress, IChatService, ChatAgentVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; -import { EditMode, IInlineChatBulkEditResponse, IInlineChatProgressItem, IInlineChatRequest, IInlineChatResponse, IInlineChatService, IInlineChatSession, IInlineChatSessionProvider, IInlineChatSlashCommand, InlineChatResponseFeedbackKind, InlineChatResponseType } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { ChatAgentLocation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; +import { CTX_INLINE_CHAT_HAS_PROVIDER, EditMode, IInlineChatBulkEditResponse, IInlineChatSession, IInlineChatSlashCommand, InlineChatResponseType } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { EmptyResponse, ErrorResponse, HunkData, ReplyResponse, Session, SessionExchange, SessionWholeRange, StashedSession, TelemetryData, TelemetryDataClassification } from './inlineChatSession'; import { IInlineChatSessionEndEvent, IInlineChatSessionEvent, IInlineChatSessionService, ISessionKeyComputer, Recording } from './inlineChatSessionService'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import { ISelection } from 'vs/editor/common/core/selection'; -import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; -import { Codicon } from 'vs/base/common/codicons'; -import { isEqual } from 'vs/base/common/resources'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -class BridgeAgent implements IChatAgentImplementation { - - constructor( - private readonly _data: IChatAgentData, - private readonly _sessions: ReadonlyMap, - private readonly _postLastResponse: (data: { id: string; response: ReplyResponse | ErrorResponse | EmptyResponse }) => void, - @IInstantiationService private readonly _instaService: IInstantiationService, - ) { } - - - private _findSessionDataByRequest(request: IChatAgentRequest) { - let data: SessionData | undefined; - for (const candidate of this._sessions.values()) { - if (candidate.session.chatModel.sessionId === request.sessionId) { - data = candidate; - break; - } - } - return data; - } - - async invoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, _history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { - - if (token.isCancellationRequested) { - return {}; - } - - const data = this._findSessionDataByRequest(request); - - if (!data) { - throw new Error('FAILED to find session'); - } - - const { session } = data; - - if (!session.lastInput) { - throw new Error('FAILED to find last input'); - } - - const inlineChatContextValue = request.variables.variables.find(candidate => candidate.name === _inlineChatContext)?.value; - const inlineChatContext = typeof inlineChatContextValue === 'string' && JSON.parse(inlineChatContextValue); - - const modelAltVersionIdNow = session.textModelN.getAlternativeVersionId(); - const progressEdits: TextEdit[][] = []; - - const inlineRequest: IInlineChatRequest = { - requestId: request.requestId, - prompt: request.message, - attempt: request.attempt ?? 0, - withIntentDetection: request.enableCommandDetection ?? true, - live: session.editMode !== EditMode.Preview, - previewDocument: session.textModelN.uri, - selection: inlineChatContext.selection, - wholeRange: inlineChatContext.wholeRange - }; - - const inlineProgress = new Progress(data => { - // TODO@jrieken - // if (data.message) { - // progress({ kind: 'progressMessage', content: new MarkdownString(data.message) }); - // } - // TODO@ulugbekna,jrieken should we only send data.slashCommand when having detected one? - if (data.slashCommand && !inlineRequest.prompt.startsWith('/')) { - const command = this._data.slashCommands.find(c => c.name === data.slashCommand); - progress({ kind: 'agentDetection', agentId: this._data.id, command }); - } - if (data.markdownFragment) { - progress({ kind: 'markdownContent', content: new MarkdownString(data.markdownFragment) }); - } - if (isNonEmptyArray(data.edits)) { - progressEdits.push(data.edits); - progress({ kind: 'textEdit', uri: session.textModelN.uri, edits: data.edits }); - } - }); - - let result: IInlineChatResponse | undefined | null; - let response: ReplyResponse | ErrorResponse | EmptyResponse; - - try { - result = await data.session.provider.provideResponse(session.session, inlineRequest, inlineProgress, token); - - if (result) { - if (result.message) { - inlineProgress.report({ markdownFragment: result.message.value }); - } - if (Array.isArray(result.edits)) { - inlineProgress.report({ edits: result.edits }); - } - - const markdownContents = result.message ?? new MarkdownString('', { supportThemeIcons: true, supportHtml: true, isTrusted: false }); - - const chatModelRequest = session.chatModel.getRequests().find(candidate => candidate.id === request.requestId); - - response = this._instaService.createInstance(ReplyResponse, result, markdownContents, session.textModelN.uri, modelAltVersionIdNow, progressEdits, request.requestId, chatModelRequest?.response); - - } else { - response = new EmptyResponse(); - } - - } catch (e) { - response = new ErrorResponse(e); - } - - this._postLastResponse({ id: request.requestId, response }); - - - return { - metadata: { - inlineChatResponse: result - } - }; - } - - async provideFollowups(request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { - - if (!result.metadata?.inlineChatResponse) { - return []; - } - - const data = this._findSessionDataByRequest(request); - if (!data) { - return []; - } - - const inlineFollowups = await data.session.provider.provideFollowups?.(data.session.session, result.metadata?.inlineChatResponse, token); - if (!inlineFollowups) { - return []; - } - - const chatFollowups = inlineFollowups.map(f => { - if (f.kind === 'reply') { - return { - kind: 'reply', - message: f.message, - agentId: request.agentId, - title: f.title, - tooltip: f.tooltip, - } satisfies IChatFollowup; - } else { - // TODO@jrieken update API - return undefined; - } - }); - - coalesceInPlace(chatFollowups); - return chatFollowups; - } - - provideWelcomeMessage(location: ChatAgentLocation, token: CancellationToken): string[] { - // without this provideSampleQuestions is not called - return []; - } - - async provideSampleQuestions(location: ChatAgentLocation, token: CancellationToken): Promise { - // TODO@jrieken DEBT - // (hack) this function is called while creating the session. We need the timeout to make sure this._sessions is populated. - // (hack) we have no context/session id and therefore use the first session with an active editor - await new Promise(resolve => setTimeout(resolve, 10)); - - for (const [, data] of this._sessions) { - if (data.session.session.input && data.editor.hasWidgetFocus()) { - return [{ - kind: 'reply', - agentId: _bridgeAgentId, - message: data.session.session.input, - }]; - } - } - return []; - } -} type SessionData = { editor: ICodeEditor; @@ -227,7 +47,6 @@ export class InlineChatError extends Error { } } -const _bridgeAgentId = 'brigde.editor'; const _inlineChatContext = '_inlineChatContext'; const _inlineChatDocument = '_inlineChatDocument'; @@ -264,10 +83,8 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { private readonly _keyComputers = new Map(); private _recordings: Recording[] = []; - private readonly _lastResponsesFromBridgeAgent = new LRUCache(5); constructor( - @IInlineChatService private readonly _inlineChatService: IInlineChatService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @IModelService private readonly _modelService: IModelService, @ITextModelService private readonly _textModelService: ITextModelService, @@ -280,103 +97,6 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { @IChatVariablesService chatVariableService: IChatVariablesService, ) { - const fakeProviders = this._store.add(new DisposableMap()); - - this._store.add(this._chatAgentService.onDidChangeAgents(() => { - - const providersNow = new Set(); - - for (const agent of this._chatAgentService.getActivatedAgents()) { - if (agent.id === _bridgeAgentId) { - // not interesting - continue; - } - if (!agent.locations.includes(ChatAgentLocation.Editor) || !agent.isDefault) { - // not interesting - continue; - } - providersNow.add(agent.id); - - if (!fakeProviders.has(agent.id)) { - fakeProviders.set(agent.id, _inlineChatService.addProvider(_instaService.createInstance(AgentInlineChatProvider, agent))); - this._logService.debug(`ADDED inline chat provider for agent ${agent.id}`); - } - } - - for (const [id] of fakeProviders) { - if (!providersNow.has(id)) { - fakeProviders.deleteAndDispose(id); - this._logService.debug(`REMOVED inline chat provider for agent ${id}`); - } - } - })); - - // MARK: register fake chat agent - const addOrRemoveBridgeAgent = () => { - const that = this; - const agentData: IChatAgentData = { - id: _bridgeAgentId, - name: 'editor', - extensionId: nullExtensionDescription.identifier, - publisherDisplayName: '', - extensionDisplayName: '', - extensionPublisherId: '', - isDefault: true, - locations: [ChatAgentLocation.Editor], - get slashCommands(): IChatAgentCommand[] { - // HACK@jrieken - // find the active session and return its slash commands - let candidate: Session | undefined; - for (const data of that._sessions.values()) { - if (data.editor.hasWidgetFocus()) { - candidate = data.session; - break; - } - } - if (!candidate || !candidate.session.slashCommands) { - return []; - } - return candidate.session.slashCommands.map(c => { - return { - name: c.command, - description: c.detail ?? '', - } satisfies IChatAgentCommand; - }); - }, - defaultImplicitVariables: [_inlineChatContext], - metadata: { - isSticky: false, - themeIcon: Codicon.copilot, - }, - }; - - let otherEditorAgent: IChatAgentData | undefined; - let myEditorAgent: IChatAgentData | undefined; - - for (const candidate of this._chatAgentService.getActivatedAgents()) { - if (!myEditorAgent && candidate.id === agentData.id) { - myEditorAgent = candidate; - } else if (!otherEditorAgent && candidate.isDefault && candidate.locations.includes(ChatAgentLocation.Editor)) { - otherEditorAgent = candidate; - } - } - - if (otherEditorAgent) { - bridgeStore.clear(); - _logService.debug(`REMOVED bridge agent "${agentData.id}", found "${otherEditorAgent.id}"`); - - } else if (!myEditorAgent) { - bridgeStore.value = this._chatAgentService.registerDynamicAgent(agentData, this._instaService.createInstance(BridgeAgent, agentData, this._sessions, data => { - this._lastResponsesFromBridgeAgent.set(data.id, data.response); - })); - _logService.debug(`ADDED bridge agent "${agentData.id}"`); - } - }; - - this._store.add(this._chatAgentService.onDidChangeAgents(() => addOrRemoveBridgeAgent())); - const bridgeStore = this._store.add(new MutableDisposable()); - addOrRemoveBridgeAgent(); - // MARK: implicit variable for editor selection and (tracked) whole range @@ -414,47 +134,34 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { async createSession(editor: IActiveCodeEditor, options: { editMode: EditMode; wholeRange?: Range }, token: CancellationToken): Promise { const agent = this._chatAgentService.getDefaultAgent(ChatAgentLocation.Editor); - let provider: IInlineChatSessionProvider | undefined; - if (agent) { - for (const candidate of this._inlineChatService.getAllProvider()) { - if (candidate instanceof AgentInlineChatProvider && candidate.agent === agent) { - provider = candidate; - break; - } - } - } - if (!provider) { - provider = Iterable.first(this._inlineChatService.getAllProvider()); - } - - if (!provider) { - this._logService.trace('[IE] NO provider found'); + if (!agent) { + this._logService.trace('[IE] NO agent found'); return undefined; } + this._onWillStartSession.fire(editor); const textModel = editor.getModel(); const selection = editor.getSelection(); - let rawSession: IInlineChatSession | undefined | null; - try { - rawSession = await raceCancellation( - Promise.resolve(provider.prepareInlineChatSession(textModel, selection, token)), - token - ); - } catch (error) { - this._logService.error('[IE] FAILED to prepare session', provider.extensionId); - this._logService.error(error); - throw new InlineChatError((error as Error)?.message || 'Failed to prepare session'); - } - if (!rawSession) { - this._logService.trace('[IE] NO session', provider.extensionId); - return undefined; - } + + const rawSession: IInlineChatSession = { + id: Math.random(), + wholeRange: new Range(selection.selectionStartLineNumber, selection.selectionStartColumn, selection.positionLineNumber, selection.positionColumn), + placeholder: agent.description, + slashCommands: agent.slashCommands.map(agentCommand => { + return { + command: agentCommand.name, + detail: agentCommand.description, + refer: agentCommand.name === 'explain' // TODO@jrieken @joyceerhl this should be cleaned up + } satisfies IInlineChatSlashCommand; + }) + }; + const store = new DisposableStore(); - this._logService.trace(`[IE] creating NEW session for ${editor.getId()}, ${provider.extensionId}`); + this._logService.trace(`[IE] creating NEW session for ${editor.getId()}, ${agent.extensionId}`); const chatModel = this._chatService.startSession(ChatAgentLocation.Editor, token); if (!chatModel) { @@ -486,59 +193,52 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { lastResponseListener.clear(); // ONCE let inlineResponse: ErrorResponse | EmptyResponse | ReplyResponse; - if (response.agent?.id === _bridgeAgentId) { - // use result that was provided by - inlineResponse = this._lastResponsesFromBridgeAgent.get(response.requestId) ?? new ErrorResponse(new Error('Missing Response')); - this._lastResponsesFromBridgeAgent.delete(response.requestId); + // make an response from the ChatResponseModel + if (response.isCanceled) { + // error: cancelled + inlineResponse = new ErrorResponse(new CancellationError()); + } else if (response.result?.errorDetails) { + // error: "real" error + inlineResponse = new ErrorResponse(new Error(response.result.errorDetails.message)); + } else if (response.response.value.length === 0) { + // epmty response + inlineResponse = new EmptyResponse(); } else { - // make an artificial response from the ChatResponseModel - if (response.isCanceled) { - // error: cancelled - inlineResponse = new ErrorResponse(new CancellationError()); - } else if (response.result?.errorDetails) { - // error: "real" error - inlineResponse = new ErrorResponse(new Error(response.result.errorDetails.message)); - } else if (response.response.value.length === 0) { - // epmty response - inlineResponse = new EmptyResponse(); - } else { - // replay response - const markdownContent = new MarkdownString(); - const raw: IInlineChatBulkEditResponse = { - id: Math.random(), - type: InlineChatResponseType.BulkEdit, - message: markdownContent, - edits: { edits: [] }, - }; - for (const item of response.response.value) { - if (item.kind === 'markdownContent') { - markdownContent.value += item.content.value; - } else if (item.kind === 'textEditGroup') { - for (const group of item.edits) { - for (const edit of group) { - raw.edits.edits.push({ - resource: item.uri, - textEdit: edit, - versionId: undefined - }); - } + // replay response + const markdownContent = new MarkdownString(); + const raw: IInlineChatBulkEditResponse = { + id: Math.random(), + type: InlineChatResponseType.BulkEdit, + message: markdownContent, + edits: { edits: [] }, + }; + for (const item of response.response.value) { + if (item.kind === 'markdownContent') { + markdownContent.value += item.content.value; + } else if (item.kind === 'textEditGroup') { + for (const group of item.edits) { + for (const edit of group) { + raw.edits.edits.push({ + resource: item.uri, + textEdit: edit, + versionId: undefined + }); } } } - - inlineResponse = this._instaService.createInstance( - ReplyResponse, - raw, - markdownContent, - session.textModelN.uri, - modelAltVersionIdNow, - [], - e.request.id, - e.request.response - ); - } + + inlineResponse = this._instaService.createInstance( + ReplyResponse, + raw, + markdownContent, + session.textModelN.uri, + modelAltVersionIdNow, + [], + e.request.id, + e.request.response + ); } session.addExchange(new SessionExchange(session.lastInput!, inlineResponse)); @@ -551,38 +251,9 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { }); })); - store.add(this._chatService.onDidPerformUserAction(e => { - if (e.sessionId !== chatModel.sessionId) { - return; - } - - // TODO@jrieken VALIDATE candidate is proper, e.g check with `session.exchanges` - const request = chatModel.getRequests().find(request => request.id === e.requestId); - const candidate = request?.response?.result?.metadata?.inlineChatResponse; - - if (!candidate) { - return; - } - - let kind: InlineChatResponseFeedbackKind | undefined; - if (e.action.kind === 'vote') { - kind = e.action.direction === ChatAgentVoteDirection.Down ? InlineChatResponseFeedbackKind.Unhelpful : InlineChatResponseFeedbackKind.Helpful; - } else if (e.action.kind === 'bug') { - kind = InlineChatResponseFeedbackKind.Bug; - } else if (e.action.kind === 'inlineChat') { - kind = e.action.action === 'accepted' ? InlineChatResponseFeedbackKind.Accepted : InlineChatResponseFeedbackKind.Undone; - } - - if (!kind) { - return; - } - - provider.handleInlineChatResponseFeedback?.(rawSession, candidate, kind); - })); - - store.add(this._inlineChatService.onDidChangeProviders(e => { - if (e.removed === provider) { - this._logService.trace(`[IE] provider GONE for ${editor.getId()}, ${provider.extensionId}`); + store.add(this._chatAgentService.onDidChangeAgents(e => { + if (e === undefined && !this._chatAgentService.getAgent(agent.id)) { + this._logService.trace(`[IE] provider GONE for ${editor.getId()}, ${agent.extensionId}`); this._releaseSession(session, true); } })); @@ -625,7 +296,8 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { targetUri, textModel0, textModelN, - provider, rawSession, + agent, + rawSession, store.add(new SessionWholeRange(textModelN, wholeRange)), store.add(new HunkData(this._editorWorkerService, textModel0, textModelN)), chatModel @@ -659,7 +331,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { found = true; this._sessions.delete(oldKey); this._sessions.set(newKey, { ...data, editor: target }); - this._logService.trace(`[IE] did MOVE session for ${data.editor.getId()} to NEW EDITOR ${target.getId()}, ${session.provider.extensionId}`); + this._logService.trace(`[IE] did MOVE session for ${data.editor.getId()} to NEW EDITOR ${target.getId()}, ${session.agent.extensionId}`); this._onDidMoveSession.fire({ session, editor: target }); break; } @@ -696,7 +368,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { const [key, value] = tuple; this._sessions.delete(key); - this._logService.trace(`[IE] did RELEASED session for ${value.editor.getId()}, ${session.provider.extensionId}`); + this._logService.trace(`[IE] did RELEASED session for ${value.editor.getId()}, ${session.agent.extensionId}`); this._onDidEndSession.fire({ editor: value.editor, session, endedByExternalCause: byServer }); value.store.dispose(); @@ -706,7 +378,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { this._keepRecording(session); const result = this._instaService.createInstance(StashedSession, editor, session, undoCancelEdits); this._onDidStashSession.fire({ editor, session }); - this._logService.trace(`[IE] did STASH session for ${editor.getId()}, ${session.provider.extensionId}`); + this._logService.trace(`[IE] did STASH session for ${editor.getId()}, ${session.agent.extensionId}`); return result; } @@ -751,89 +423,24 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { } } -export class AgentInlineChatProvider implements IInlineChatSessionProvider { +export class InlineChatEnabler { - readonly extensionId: ExtensionIdentifier; - readonly label: string; - readonly supportIssueReporting?: boolean | undefined; + static Id = 'inlineChat.enabler'; + + private readonly _ctxHasProvider: IContextKey; constructor( - readonly agent: IChatAgent, - @IChatAgentService private readonly _chatAgentService: IChatAgentService, + @IContextKeyService contextKeyService: IContextKeyService, + @IChatAgentService chatAgentService: IChatAgentService ) { - this.label = agent.fullName ?? agent.name; - this.extensionId = agent.extensionId; - this.supportIssueReporting = agent.metadata.supportIssueReporting; + this._ctxHasProvider = CTX_INLINE_CHAT_HAS_PROVIDER.bindTo(contextKeyService); + chatAgentService.onDidChangeAgents(() => { + const hasEditorAgent = Boolean(chatAgentService.getDefaultAgent(ChatAgentLocation.Editor)); + this._ctxHasProvider.set(hasEditorAgent); + }); } - async prepareInlineChatSession(model: ITextModel, range: ISelection, token: CancellationToken): Promise { - - // TODO@jrieken have a good welcome message - // const welcomeMessage = await this.agent.provideWelcomeMessage?.(ChatAgentLocation.Editor, token); - // const message = welcomeMessage?.filter(candidate => typeof candidate === 'string').join(''), - - return { - id: Math.random(), - wholeRange: new Range(range.selectionStartLineNumber, range.selectionStartColumn, range.positionLineNumber, range.positionColumn), - placeholder: this.agent.description, - slashCommands: this.agent.slashCommands.map(agentCommand => { - return { - command: agentCommand.name, - detail: agentCommand.description, - refer: agentCommand.name === 'explain' // TODO@jrieken @joyceerhl this should be cleaned up - } satisfies IInlineChatSlashCommand; - }) - }; + dispose() { + this._ctxHasProvider.reset(); } - - async provideResponse(item: IInlineChatSession, request: IInlineChatRequest, progress: IProgress, token: CancellationToken): Promise { - - const workspaceEdit: WorkspaceEdit = { edits: [] }; - - await this._chatAgentService.invokeAgent(this.agent.id, { - sessionId: String(item.id), - requestId: request.requestId, - agentId: this.agent.id, - message: request.prompt, - location: ChatAgentLocation.Editor, - variables: { - variables: [{ - id: InlineChatContext.variableName, - name: InlineChatContext.variableName, - value: JSON.stringify(new InlineChatContext(request.previewDocument, request.selection, request.wholeRange)) - }] - } - }, part => { - - if (part.kind === 'markdownContent') { - progress.report({ markdownFragment: part.content.value }); - } else if (part.kind === 'agentDetection') { - progress.report({ slashCommand: part.command?.name }); - } else if (part.kind === 'textEdit') { - - if (isEqual(request.previewDocument, part.uri)) { - progress.report({ edits: part.edits }); - } else { - for (const textEdit of part.edits) { - workspaceEdit.edits.push({ resource: part.uri, textEdit, versionId: undefined }); - } - } - } - - }, [], token); - - return { - type: InlineChatResponseType.BulkEdit, - id: Math.random(), - edits: workspaceEdit - }; - } - - // handleInlineChatResponseFeedback?(session: IInlineChatSession, response: IInlineChatResponse, kind: InlineChatResponseFeedbackKind): void { - // throw new Error('Method not implemented.'); - // } - - // provideFollowups?(session: IInlineChatSession, response: IInlineChatResponse, token: CancellationToken): ProviderResult { - // throw new Error('Method not implemented.'); - // } } diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index fc2da4a247d..59bb7cafe09 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -3,20 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IDisposable } from 'vs/base/common/lifecycle'; import { IRange } from 'vs/editor/common/core/range'; import { ISelection } from 'vs/editor/common/core/selection'; -import { ProviderResult, TextEdit, WorkspaceEdit } from 'vs/editor/common/languages'; -import { ITextModel } from 'vs/editor/common/model'; +import { TextEdit, WorkspaceEdit } from 'vs/editor/common/languages'; import { localize } from 'vs/nls'; import { MenuId } from 'vs/platform/actions/common/actions'; import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IProgress } from 'vs/platform/progress/common/progress'; import { Registry } from 'vs/platform/registry/common/platform'; import { diffInserted, diffRemoved, editorHoverHighlight, editorWidgetBackground, editorWidgetBorder, focusBorder, inputBackground, inputPlaceholderForeground, registerColor, transparent, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; import { Extensions as ExtensionsMigration, IConfigurationMigrationRegistry } from 'vs/workbench/common/configuration'; @@ -115,33 +112,46 @@ export interface IInlineChatCommandFollowup { export type IInlineChatFollowup = IInlineChatReplyFollowup | IInlineChatCommandFollowup; +/** + * @deprecated + */ export interface IInlineChatSessionProvider { extensionId: ExtensionIdentifier; label: string; - supportIssueReporting?: boolean; - prepareInlineChatSession(model: ITextModel, range: ISelection, token: CancellationToken): ProviderResult; - - provideResponse(item: IInlineChatSession, request: IInlineChatRequest, progress: IProgress, token: CancellationToken): ProviderResult; - - provideFollowups?(session: IInlineChatSession, response: IInlineChatResponse, token: CancellationToken): ProviderResult; - - handleInlineChatResponseFeedback?(session: IInlineChatSession, response: IInlineChatResponse, kind: InlineChatResponseFeedbackKind): void; } +/** + * @deprecated + */ export const IInlineChatService = createDecorator('IInlineChatService'); +/** + * @deprecated + */ export interface InlineChatProviderChangeEvent { readonly added?: IInlineChatSessionProvider; readonly removed?: IInlineChatSessionProvider; } +/** + * @deprecated + */ export interface IInlineChatService { _serviceBrand: undefined; + /** + * @deprecated + */ onDidChangeProviders: Event; + /** + * @deprecated + */ addProvider(provider: IInlineChatSessionProvider): IDisposable; + /** + * @deprecated + */ getAllProvider(): Iterable; } diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts index 8c2b1e99ff8..9570097d78f 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts @@ -30,7 +30,7 @@ import { IInlineChatSavingService } from 'vs/workbench/contrib/inlineChat/browse import { HunkState, Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; import { IInlineChatSessionService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionService'; import { InlineChatSessionServiceImpl } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl'; -import { EditMode, IInlineChatService, InlineChatResponseType } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { EditMode, IInlineChatService } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; import { CancellationToken } from 'vs/base/common/cancellation'; import { assertType } from 'vs/base/common/types'; @@ -64,7 +64,6 @@ suite('InlineChatSession', function () { let editor: IActiveCodeEditor; let model: ITextModel; let instaService: TestInstantiationService; - let inlineChatService: InlineChatServiceImpl; let inlineChatSessionService: IInlineChatSessionService; @@ -124,8 +123,6 @@ suite('InlineChatSession', function () { instaService = store.add(workbenchInstantiationService(undefined, store).createChild(serviceCollection)); - - inlineChatService = instaService.get(IInlineChatService) as InlineChatServiceImpl; inlineChatSessionService = store.add(instaService.get(IInlineChatSessionService)); instaService.get(IChatAgentService).registerDynamicAgent({ @@ -136,7 +133,7 @@ suite('InlineChatSession', function () { id: 'testAgent', name: 'testAgent', isDefault: true, - locations: [ChatAgentLocation.Panel], + locations: [ChatAgentLocation.Editor], metadata: {}, slashCommands: [] }, { @@ -145,25 +142,7 @@ suite('InlineChatSession', function () { } }); - store.add(inlineChatService.addProvider({ - extensionId: nullExtensionDescription.identifier, - label: 'Unit Test', - prepareInlineChatSession() { - return { - id: Math.random() - }; - }, - provideResponse(session, request) { - return { - type: InlineChatResponseType.EditorEdit, - id: Math.random(), - edits: [{ - range: new Range(1, 1, 1, 1), - text: request.prompt - }] - }; - } - })); + model = store.add(instaService.get(IModelService).createModel('one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\neleven', null)); editor = store.add(instantiateTestCodeEditor(instaService, model)); }); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnosticEditorContrib.ts b/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnosticEditorContrib.ts index a04c894f931..251bee99b88 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnosticEditorContrib.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnosticEditorContrib.ts @@ -7,15 +7,14 @@ import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle' import { IMarkerData, IMarkerService } from 'vs/platform/markers/common/markers'; import { IRange } from 'vs/editor/common/core/range'; import { ICellExecutionError, ICellExecutionStateChangedEvent, IExecutionStateChangedEvent, INotebookExecutionStateService, NotebookExecutionType } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; -import { IInlineChatService } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { CellKind, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookEditor, INotebookEditorContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { registerNotebookContribution } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; -import { Iterable } from 'vs/base/common/iterator'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { URI } from 'vs/base/common/uri'; import { Event } from 'vs/base/common/event'; +import { ChatAgentLocation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; type CellDiagnostic = { cellUri: URI; @@ -35,14 +34,14 @@ export class CellDiagnostics extends Disposable implements INotebookEditorContri private readonly notebookEditor: INotebookEditor, @INotebookExecutionStateService private readonly notebookExecutionStateService: INotebookExecutionStateService, @IMarkerService private readonly markerService: IMarkerService, - @IInlineChatService private readonly inlineChatService: IInlineChatService, + @IChatAgentService private readonly chatAgentService: IChatAgentService, @IConfigurationService private readonly configurationService: IConfigurationService ) { super(); this.updateEnabled(); - this._register(inlineChatService.onDidChangeProviders(() => this.updateEnabled())); + this._register(chatAgentService.onDidChangeAgents(() => this.updateEnabled())); this._register(configurationService.onDidChangeConfiguration((e) => { if (e.affectsConfiguration(NotebookSetting.cellFailureDiagnostics)) { this.updateEnabled(); @@ -52,10 +51,10 @@ export class CellDiagnostics extends Disposable implements INotebookEditorContri private updateEnabled() { const settingEnabled = this.configurationService.getValue(NotebookSetting.cellFailureDiagnostics); - if (this.enabled && (!settingEnabled || Iterable.isEmpty(this.inlineChatService.getAllProvider()))) { + if (this.enabled && (!settingEnabled || !this.chatAgentService.getDefaultAgent(ChatAgentLocation.Editor))) { this.enabled = false; this.clearAll(); - } else if (!this.enabled && settingEnabled && !Iterable.isEmpty(this.inlineChatService.getAllProvider())) { + } else if (!this.enabled && settingEnabled && !!this.chatAgentService.getDefaultAgent(ChatAgentLocation.Editor)) { this.enabled = true; if (!this.listening) { this.listening = true; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/editorHint/emptyCellEditorHint.ts b/src/vs/workbench/contrib/notebook/browser/contrib/editorHint/emptyCellEditorHint.ts index 2ee5da0b02b..eeef10e64d2 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/editorHint/emptyCellEditorHint.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/editorHint/emptyCellEditorHint.ts @@ -12,9 +12,9 @@ import { IHoverService } from 'vs/platform/hover/browser/hover'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IProductService } from 'vs/platform/product/common/productService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { EmptyTextEditorHintContribution, IEmptyTextEditorHintOptions } from 'vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint'; import { IInlineChatSessionService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionService'; -import { IInlineChatService } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { getNotebookEditorFromEditorPane } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -30,7 +30,7 @@ export class EmptyCellEditorHintContribution extends EmptyTextEditorHintContribu @IHoverService hoverService: IHoverService, @IKeybindingService keybindingService: IKeybindingService, @IInlineChatSessionService inlineChatSessionService: IInlineChatSessionService, - @IInlineChatService inlineChatService: IInlineChatService, + @IChatAgentService chatAgentService: IChatAgentService, @ITelemetryService telemetryService: ITelemetryService, @IProductService productService: IProductService ) { @@ -42,7 +42,7 @@ export class EmptyCellEditorHintContribution extends EmptyTextEditorHintContribu hoverService, keybindingService, inlineChatSessionService, - inlineChatService, + chatAgentService, telemetryService, productService ); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/test/browser/terminalInitialHint.test.ts b/src/vs/workbench/contrib/terminalContrib/chat/test/browser/terminalInitialHint.test.ts index 3cd6c48a21e..c92241f6741 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/test/browser/terminalInitialHint.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/test/browser/terminalInitialHint.test.ts @@ -10,13 +10,10 @@ import { workbenchInstantiationService } from 'vs/workbench/test/browser/workben import { NullLogService } from 'vs/platform/log/common/log'; import { InitialHintAddon } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution'; import { getActiveDocument } from 'vs/base/browser/dom'; -import { IInlineChatSession, IInlineChatSessionProvider, InlineChatProviderChangeEvent } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { IInlineChatSessionProvider, InlineChatProviderChangeEvent } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { Emitter } from 'vs/base/common/event'; import { strictEqual } from 'assert'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { ITextModel } from 'vs/editor/common/model'; -import { ISelection } from 'vs/editor/common/core/selection'; -import { CancellationToken } from 'vs/base/common/cancellation'; // Test TerminalInitialHintAddon @@ -51,13 +48,7 @@ suite('Terminal Initial Hint Addon', () => { eventCount = 0; const provider: IInlineChatSessionProvider = { extensionId: new ExtensionIdentifier('test'), - label: 'blahblah', - prepareInlineChatSession(model: ITextModel, range: ISelection, token: CancellationToken): Promise { - throw new Error('Method not implemented.'); - }, - provideResponse() { - throw new Error('Method not implemented.'); - } + label: 'blahblah' }; _onDidChangeProviders.fire({ added: provider }); xterm.focus(); @@ -68,13 +59,7 @@ suite('Terminal Initial Hint Addon', () => { test('hint is not shown when there has been input', () => { const provider: IInlineChatSessionProvider = { extensionId: new ExtensionIdentifier('test'), - label: 'blahblah', - prepareInlineChatSession(model: ITextModel, range: ISelection, token: CancellationToken): Promise { - throw new Error('Method not implemented.'); - }, - provideResponse() { - throw new Error('Method not implemented.'); - } + label: 'blahblah' }; _onDidChangeProviders.fire({ added: provider }); xterm.writeln('data'); @@ -85,5 +70,3 @@ suite('Terminal Initial Hint Addon', () => { }); }); }); - - From 51bee127fa30c8839ca463f9e01f6406a5790d3d Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 21 May 2024 17:53:58 +0200 Subject: [PATCH 293/357] undo cell diag changes --- .../cellDiagnostics/cellDiagnosticEditorContrib.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnosticEditorContrib.ts b/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnosticEditorContrib.ts index 251bee99b88..a04c894f931 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnosticEditorContrib.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnosticEditorContrib.ts @@ -7,14 +7,15 @@ import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle' import { IMarkerData, IMarkerService } from 'vs/platform/markers/common/markers'; import { IRange } from 'vs/editor/common/core/range'; import { ICellExecutionError, ICellExecutionStateChangedEvent, IExecutionStateChangedEvent, INotebookExecutionStateService, NotebookExecutionType } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; +import { IInlineChatService } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { CellKind, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookEditor, INotebookEditorContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { registerNotebookContribution } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; +import { Iterable } from 'vs/base/common/iterator'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { URI } from 'vs/base/common/uri'; import { Event } from 'vs/base/common/event'; -import { ChatAgentLocation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; type CellDiagnostic = { cellUri: URI; @@ -34,14 +35,14 @@ export class CellDiagnostics extends Disposable implements INotebookEditorContri private readonly notebookEditor: INotebookEditor, @INotebookExecutionStateService private readonly notebookExecutionStateService: INotebookExecutionStateService, @IMarkerService private readonly markerService: IMarkerService, - @IChatAgentService private readonly chatAgentService: IChatAgentService, + @IInlineChatService private readonly inlineChatService: IInlineChatService, @IConfigurationService private readonly configurationService: IConfigurationService ) { super(); this.updateEnabled(); - this._register(chatAgentService.onDidChangeAgents(() => this.updateEnabled())); + this._register(inlineChatService.onDidChangeProviders(() => this.updateEnabled())); this._register(configurationService.onDidChangeConfiguration((e) => { if (e.affectsConfiguration(NotebookSetting.cellFailureDiagnostics)) { this.updateEnabled(); @@ -51,10 +52,10 @@ export class CellDiagnostics extends Disposable implements INotebookEditorContri private updateEnabled() { const settingEnabled = this.configurationService.getValue(NotebookSetting.cellFailureDiagnostics); - if (this.enabled && (!settingEnabled || !this.chatAgentService.getDefaultAgent(ChatAgentLocation.Editor))) { + if (this.enabled && (!settingEnabled || Iterable.isEmpty(this.inlineChatService.getAllProvider()))) { this.enabled = false; this.clearAll(); - } else if (!this.enabled && settingEnabled && !!this.chatAgentService.getDefaultAgent(ChatAgentLocation.Editor)) { + } else if (!this.enabled && settingEnabled && !Iterable.isEmpty(this.inlineChatService.getAllProvider())) { this.enabled = true; if (!this.listening) { this.listening = true; From 3bda9ff4f70e298163fcd29c5083596deb26527f Mon Sep 17 00:00:00 2001 From: Walker Boyle Date: Tue, 21 May 2024 11:01:17 -0700 Subject: [PATCH 294/357] fix: tsserver no longer crashes when log path includes spaces (#212752) --- .../typescript-language-features/src/tsServer/spawner.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/typescript-language-features/src/tsServer/spawner.ts b/extensions/typescript-language-features/src/tsServer/spawner.ts index b88251a7033..bc55ab4fdb3 100644 --- a/extensions/typescript-language-features/src/tsServer/spawner.ts +++ b/extensions/typescript-language-features/src/tsServer/spawner.ts @@ -234,7 +234,7 @@ export class TypeScriptServerSpawner { tsServerLog = { type: 'file', uri: logFilePath }; args.push('--logVerbosity', TsServerLogLevel.toString(configuration.tsServerLogLevel)); - args.push('--logFile', logFilePath.fsPath); + args.push('--logFile', `"${logFilePath.fsPath}"`); } } } @@ -242,7 +242,7 @@ export class TypeScriptServerSpawner { if (configuration.enableTsServerTracing && !isWeb()) { tsServerTraceDirectory = this._logDirectoryProvider.getNewLogDirectory(); if (tsServerTraceDirectory) { - args.push('--traceDirectory', tsServerTraceDirectory.fsPath); + args.push('--traceDirectory', `"${tsServerTraceDirectory.fsPath}"`); } } From 0e913580f154fb58ca9d8ce7e3a47d99f05dce6a Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Tue, 21 May 2024 11:31:43 -0700 Subject: [PATCH 295/357] =?UTF-8?q?Treat=C2=A0normal=20markdown=20links=20?= =?UTF-8?q?as=20a=20single=20token=20for=20progressive=20chat=20rendering?= =?UTF-8?q?=20(#213091)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes our progressive chat renderer breaking up any markdown links that contain `-` or a space character. Doesn't support all the fancy markdown link syntaxes, just basic links for now --- .../contrib/chat/common/chatWordCounter.ts | 27 +++++++------------ .../chat/test/common/chatWordCounter.test.ts | 11 ++++++++ 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/chatWordCounter.ts b/src/vs/workbench/contrib/chat/common/chatWordCounter.ts index eeb2d6691fb..94870296160 100644 --- a/src/vs/workbench/contrib/chat/common/chatWordCounter.ts +++ b/src/vs/workbench/contrib/chat/common/chatWordCounter.ts @@ -3,8 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -const wordSeparatorCharPattern = /[\s\|\-]/; - export interface IWordCountResult { value: string; actualWordCount: number; @@ -12,27 +10,20 @@ export interface IWordCountResult { } export function getNWords(str: string, numWordsToCount: number): IWordCountResult { - let wordCount = numWordsToCount; - let i = 0; - while (i < str.length && wordCount > 0) { - // Consume word separator chars - while (i < str.length && str[i].match(wordSeparatorCharPattern)) { - i++; - } + // Match words and markdown style links + const allWordMatches = Array.from(str.matchAll(/\[([^\]]+)\]\(([^)]+)\)|[^\s\|\-]+/g)); - // Consume word chars - while (i < str.length && !str[i].match(wordSeparatorCharPattern)) { - i++; - } + const targetWords = allWordMatches.slice(0, numWordsToCount); - wordCount--; - } + const endIndex = numWordsToCount > allWordMatches.length + ? str.length // Reached end of string + : targetWords.length ? targetWords.at(-1)!.index + targetWords.at(-1)![0].length : 0; - const value = str.substring(0, i); + const value = str.substring(0, endIndex); return { value, - actualWordCount: numWordsToCount - wordCount, - isFullString: i >= str.length + actualWordCount: targetWords.length === 0 ? (value.length ? 1 : 0) : targetWords.length, + isFullString: endIndex >= str.length }; } diff --git a/src/vs/workbench/contrib/chat/test/common/chatWordCounter.test.ts b/src/vs/workbench/contrib/chat/test/common/chatWordCounter.test.ts index 9441aacda6b..314b4589e31 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatWordCounter.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatWordCounter.test.ts @@ -29,4 +29,15 @@ suite('ChatWordCounter', () => { cases.forEach(([str, nWords, result]) => doTest(str, nWords, result)); }); + + test('getNWords, matching links', () => { + const cases: [string, number, string][] = [ + ['[hello](https://example.com) world', 1, '[hello](https://example.com)'], + ['[hello](https://example.com) world', 2, '[hello](https://example.com) world'], + ['oh [hello](https://example.com "title") world', 1, 'oh'], + ['oh [hello](https://example.com "title") world', 2, 'oh [hello](https://example.com "title")'], + ]; + + cases.forEach(([str, nWords, result]) => doTest(str, nWords, result)); + }); }); From 6743fa305c89d611f11ce637d409b6c2e127a404 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Tue, 21 May 2024 12:33:34 -0700 Subject: [PATCH 296/357] Pick up latest ts for building VS Code (#213170) --- build/lib/util.js | 1 - package.json | 2 +- yarn.lock | 39 +++++++-------------------------------- 3 files changed, 8 insertions(+), 34 deletions(-) diff --git a/build/lib/util.js b/build/lib/util.js index ed52776c2c0..02ce049b00b 100644 --- a/build/lib/util.js +++ b/build/lib/util.js @@ -34,7 +34,6 @@ const rename = require("gulp-rename"); const path = require("path"); const fs = require("fs"); const _rimraf = require("rimraf"); -const VinylFile = require("vinyl"); const url_1 = require("url"); const ternaryStream = require("ternary-stream"); const root = path.dirname(path.dirname(__dirname)); diff --git a/package.json b/package.json index b74ee912c95..2f22c3ef924 100644 --- a/package.json +++ b/package.json @@ -207,7 +207,7 @@ "ts-loader": "^9.4.2", "ts-node": "^10.9.1", "tsec": "0.2.7", - "typescript": "^5.5.0-dev.20240506", + "typescript": "^5.5.0-dev.20240521", "util": "^0.12.4", "vscode-nls-dev": "^3.3.1", "webpack": "^5.91.0", diff --git a/yarn.lock b/yarn.lock index 1d866e83a1f..438959bf7de 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9290,7 +9290,7 @@ streamx@^2.15.0: fast-fifo "^1.1.0" queue-tick "^1.0.1" -"string-width-cjs@npm:string-width@^4.2.0": +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -9334,15 +9334,6 @@ string-width@^4.1.0, string-width@^4.2.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" -string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -9396,7 +9387,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -9431,13 +9422,6 @@ strip-ansi@^6.0.0: dependencies: ansi-regex "^5.0.0" -strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -10040,10 +10024,10 @@ typescript@^4.7.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6" integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ== -typescript@^5.5.0-dev.20240506: - version "5.5.0-dev.20240506" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.0-dev.20240506.tgz#d46aac8be07432092e3bd7e7fb7aae93b21d738a" - integrity sha512-0lnovJfyTASSjJvryIfT3sYDXAv1n2R0vujhtdXQiAxA+PRpCOTk7UqslELD6wl7t3s9hH5UI/+p5aPeSpmbYw== +typescript@^5.5.0-dev.20240521: + version "5.5.0-dev.20240521" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.0-dev.20240521.tgz#a53f71ad2f5e4c4401a56c35993474b77813364c" + integrity sha512-52WLKX9mbRmStK1lb30KM78dSo5ssgQT8WQERYiv8JihXir4HUgwlgTz4crExojzpsGjFGFJROL/bZrhXUiOEQ== typical@^4.0.0: version "4.0.0" @@ -10583,7 +10567,7 @@ workerpool@6.2.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -10618,15 +10602,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From f2f4adc8063784eece7c4da515adf518033f1a0a Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Tue, 21 May 2024 12:34:09 -0700 Subject: [PATCH 297/357] feat: allow extensions to attach chat context (#213172) * feat: allow extensions to attach chat context * fix: allow attaching different ranges from same file * fix: restore accepting files from picker --- .../api/browser/mainThreadChatVariables.ts | 7 +++++ .../workbench/api/common/extHost.api.impl.ts | 4 +++ .../workbench/api/common/extHost.protocol.ts | 2 ++ .../api/common/extHostChatVariables.ts | 4 +++ .../api/common/extHostTypeConverters.ts | 13 +++++++- .../browser/actions/chatContextActions.ts | 2 ++ .../contrib/chat/browser/chatInputPart.ts | 2 ++ .../contrib/chat/browser/chatVariables.ts | 30 +++++++++++++++++++ .../contrib/chat/browser/media/chat.css | 4 +++ .../contrib/chat/common/chatVariables.ts | 2 ++ .../chat/test/common/mockChatVariables.ts | 5 ++++ .../vscode.proposed.chatVariableResolver.d.ts | 9 ++++++ 12 files changed, 83 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/api/browser/mainThreadChatVariables.ts b/src/vs/workbench/api/browser/mainThreadChatVariables.ts index 9e08e5d1423..bf7103206a0 100644 --- a/src/vs/workbench/api/browser/mainThreadChatVariables.ts +++ b/src/vs/workbench/api/browser/mainThreadChatVariables.ts @@ -5,7 +5,10 @@ import { DisposableMap } from 'vs/base/common/lifecycle'; import { revive } from 'vs/base/common/marshalling'; +import { URI } from 'vs/base/common/uri'; +import { Location } from 'vs/editor/common/languages'; import { ExtHostChatVariablesShape, ExtHostContext, IChatVariableResolverProgressDto, MainContext, MainThreadChatVariablesShape } from 'vs/workbench/api/common/extHost.protocol'; +import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatRequestVariableValue, IChatVariableData, IChatVariableResolverProgress, IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; @@ -47,4 +50,8 @@ export class MainThreadChatVariables implements MainThreadChatVariablesShape { $unregisterVariable(handle: number): void { this._variables.deleteAndDispose(handle); } + + $attachContext(name: string, value: string | URI | Location | unknown, location: ChatAgentLocation.Panel): void { + this._chatVariablesService.attachContext(name, revive(value), location); + } } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 04372c16964..a2cbad65c65 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1425,6 +1425,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I createDynamicChatParticipant(id: string, dynamicProps: vscode.DynamicChatParticipantProps, handler: vscode.ChatExtendedRequestHandler): vscode.ChatParticipant { checkProposedApiEnabled(extension, 'chatParticipantAdditions'); return extHostChatAgents2.createDynamicChatAgent(extension, id, dynamicProps, handler); + }, + attachContext(name: string, value: string | vscode.Uri | vscode.Location | unknown, location: vscode.ChatLocation.Panel) { + checkProposedApiEnabled(extension, 'chatVariableResolver'); + return extHostChatVariables.attachContext(name, value, location); } }; diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 441388971aa..2e0a39fb1a5 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -6,6 +6,7 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IRemoteConsoleLog } from 'vs/base/common/console'; +import { Location } from 'vs/editor/common/languages'; import { SerializedError } from 'vs/base/common/errors'; import { IRelativePattern } from 'vs/base/common/glob'; import { IMarkdownString } from 'vs/base/common/htmlContent'; @@ -1290,6 +1291,7 @@ export interface MainThreadChatVariablesShape extends IDisposable { $registerVariable(handle: number, data: IChatVariableData): void; $handleProgressChunk(requestId: string, progress: IChatVariableResolverProgressDto): Promise; $unregisterVariable(handle: number): void; + $attachContext(name: string, value: string | Dto | URI | unknown, location: ChatAgentLocation): void; } export type IChatRequestVariableValueDto = Dto; diff --git a/src/vs/workbench/api/common/extHostChatVariables.ts b/src/vs/workbench/api/common/extHostChatVariables.ts index dfc37201bd4..5f0bf7d2449 100644 --- a/src/vs/workbench/api/common/extHostChatVariables.ts +++ b/src/vs/workbench/api/common/extHostChatVariables.ts @@ -64,6 +64,10 @@ export class ExtHostChatVariables implements ExtHostChatVariablesShape { this._proxy.$unregisterVariable(handle); }); } + + attachContext(name: string, value: string | vscode.Location | vscode.Uri | unknown, location: vscode.ChatLocation.Panel) { + this._proxy.$attachContext(name, extHostTypes.Location.isLocation(value) ? typeConvert.Location.from(value) : value, typeConvert.ChatLocation.from(location)); + } } class ChatVariableResolverResponseStream { diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 6ae92cd7ca3..6525d0f2009 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -2566,6 +2566,15 @@ export namespace ChatLocation { case ChatAgentLocation.Editor: return types.ChatLocation.Editor; } } + + export function from(loc: types.ChatLocation): ChatAgentLocation { + switch (loc) { + case types.ChatLocation.Notebook: return ChatAgentLocation.Notebook; + case types.ChatLocation.Terminal: return ChatAgentLocation.Terminal; + case types.ChatLocation.Panel: return ChatAgentLocation.Panel; + case types.ChatLocation.Editor: return ChatAgentLocation.Editor; + } + } } export namespace ChatAgentValueReference { @@ -2579,7 +2588,9 @@ export namespace ChatAgentValueReference { id: variable.id, name: variable.name, range: variable.range && [variable.range.start, variable.range.endExclusive], - value: isUriComponents(value) ? URI.revive(value) : value, + value: isUriComponents(value) ? URI.revive(value) : + value && typeof value === 'object' && 'uri' in value && 'range' in value && isUriComponents(value.uri) ? + Location.to(revive(value)) : value, modelDescription: variable.modelDescription }; } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index b810ee50edf..80a697a7047 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -70,6 +70,8 @@ class AttachContextAction extends Action2 { toAttach.push({ ...pick, isDynamic: pick.isDynamic, value: pick.value, name: qualifiedName, fullName: `$(${pick.icon.id}) ${selection}` }); } + } else if (pick && typeof pick === 'object' && 'resource' in pick) { + toAttach.push({ ...pick, value: pick.resource }); } else { toAttach.push({ ...pick, fullName: pick.label }); } diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index f34ead61db1..d857c4cc959 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -7,6 +7,7 @@ import * as dom from 'vs/base/browser/dom'; import { DEFAULT_FONT_FAMILY } from 'vs/base/browser/fonts'; import { IHistoryNavigationWidget } from 'vs/base/browser/history'; import * as aria from 'vs/base/browser/ui/aria/aria'; +import { Range } from 'vs/editor/common/core/range'; import { Button } from 'vs/base/browser/ui/button/button'; import { IAction } from 'vs/base/common/actions'; import { Codicon } from 'vs/base/common/codicons'; @@ -442,6 +443,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge label.setFile(file, { fileKind: FileKind.FILE, hidePath: true, + range: attachment.value && typeof attachment.value === 'object' && 'range' in attachment.value && Range.isIRange(attachment.value.range) ? attachment.value.range : undefined, }); } else { label.setLabel(attachment.fullName ?? attachment.name); diff --git a/src/vs/workbench/contrib/chat/browser/chatVariables.ts b/src/vs/workbench/contrib/chat/browser/chatVariables.ts index bdf8b8e2a5b..5207c6d6a53 100644 --- a/src/vs/workbench/contrib/chat/browser/chatVariables.ts +++ b/src/vs/workbench/contrib/chat/browser/chatVariables.ts @@ -3,13 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { basename } from 'vs/base/common/path'; import { coalesce } from 'vs/base/common/arrays'; import { CancellationToken } from 'vs/base/common/cancellation'; import { onUnexpectedExternalError } from 'vs/base/common/errors'; import { Iterable } from 'vs/base/common/iterator'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { Location } from 'vs/editor/common/languages'; import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatDynamicVariableModel } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables'; +import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatModel, IChatRequestVariableData, IChatRequestVariableEntry } from 'vs/workbench/contrib/chat/common/chatModel'; import { ChatRequestDynamicVariablePart, ChatRequestVariablePart, IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatContentReference } from 'vs/workbench/contrib/chat/common/chatService'; @@ -142,4 +146,30 @@ export class ChatVariablesService implements IChatVariablesService { this._resolver.delete(key); }); } + + async attachContext(name: string, value: string | URI | Location, location: ChatAgentLocation) { + if (location !== ChatAgentLocation.Panel) { + return; + } + + const widget = this.chatWidgetService.lastFocusedWidget; + if (!widget || !widget.viewModel) { + return; + } + + const key = name.toLowerCase(); + if (key === 'file' && typeof value !== 'string') { + const uri = URI.isUri(value) ? value : value.uri; + const range = 'range' in value ? value.range : undefined; + widget.attachContext({ value, id: uri.toString() + (range?.toString() ?? ''), name: basename(uri.path), isDynamic: true }); + return; + } + + const resolved = this._resolver.get(key); + if (!resolved) { + return; + } + + widget.attachContext({ ...resolved.data, value }); + } } diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 5c37a078b78..63f14ff2492 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -493,6 +493,10 @@ align-items: center; } +.interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-icon-label-container { + display: flex; +} + .interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-icon-label-container .monaco-highlighted-label { display: flex !important; align-items: center !important; diff --git a/src/vs/workbench/contrib/chat/common/chatVariables.ts b/src/vs/workbench/contrib/chat/common/chatVariables.ts index 09f9fd2396d..1df71e988eb 100644 --- a/src/vs/workbench/contrib/chat/common/chatVariables.ts +++ b/src/vs/workbench/contrib/chat/common/chatVariables.ts @@ -10,6 +10,7 @@ import { URI } from 'vs/base/common/uri'; import { IRange } from 'vs/editor/common/core/range'; import { Location } from 'vs/editor/common/languages'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatModel, IChatRequestVariableData, IChatRequestVariableEntry } from 'vs/workbench/contrib/chat/common/chatModel'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatContentReference, IChatProgressMessage } from 'vs/workbench/contrib/chat/common/chatService'; @@ -45,6 +46,7 @@ export interface IChatVariablesService { getVariable(name: string): IChatVariableData | undefined; getVariables(): Iterable>; getDynamicVariables(sessionId: string): ReadonlyArray; // should be its own service? + attachContext(name: string, value: string | URI | Location | unknown, location: ChatAgentLocation): void; /** * Resolves all variables that occur in `prompt` diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatVariables.ts b/src/vs/workbench/contrib/chat/test/common/mockChatVariables.ts index d18f9b473df..be7a61b525e 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatVariables.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatVariables.ts @@ -5,6 +5,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatModel, IChatRequestVariableData, IChatRequestVariableEntry } from 'vs/workbench/contrib/chat/common/chatModel'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatRequestVariableValue, IChatVariableData, IChatVariableResolver, IChatVariableResolverProgress, IChatVariablesService, IDynamicVariable } from 'vs/workbench/contrib/chat/common/chatVariables'; @@ -37,6 +38,10 @@ export class MockChatVariablesService implements IChatVariablesService { }; } + attachContext(name: string, value: unknown, location: ChatAgentLocation): void { + throw new Error('Method not implemented.'); + } + resolveVariable(variableName: string, promptText: string, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise { throw new Error('Method not implemented.'); } diff --git a/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts b/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts index 91380192de2..298fc82f61b 100644 --- a/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts +++ b/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts @@ -19,6 +19,15 @@ declare module 'vscode' { * @param icon An icon to display when selecting context in the picker UI. */ export function registerChatVariableResolver(id: string, name: string, userDescription: string, modelDescription: string | undefined, isSlow: boolean | undefined, resolver: ChatVariableResolver, fullName?: string, icon?: ThemeIcon): Disposable; + + /** + * Attaches a chat context with the specified name, value, and location. + * + * @param name - The name of the chat context. + * @param value - The value of the chat context. + * @param location - The location of the chat context. + */ + export function attachContext(name: string, value: string | Uri | Location | unknown, location: ChatLocation.Panel): void; } export interface ChatVariableValue { From ba9fe552f4e8e09c3e8d500c358f9562b1769dd8 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 21 May 2024 13:09:31 -0700 Subject: [PATCH 298/357] Use `resolveContentAndKeybindingItems` for diff editor, accessible view, and terminal (#213179) --- .../accessibility/browser/accessibleView.ts | 1 + .../accessibility/browser/accessibleView.ts | 84 ++++++------------- .../browser/accessibleViewActions.ts | 19 +++++ .../browser/accessibleViewContributions.ts | 11 --- .../common/accessibilityCommands.ts | 1 + .../browser/diffEditorAccessibilityHelp.ts | 13 +-- .../browser/terminalAccessibilityHelp.ts | 42 ++-------- 7 files changed, 58 insertions(+), 113 deletions(-) diff --git a/src/vs/platform/accessibility/browser/accessibleView.ts b/src/vs/platform/accessibility/browser/accessibleView.ts index 7674bf33f24..d2fedcb8661 100644 --- a/src/vs/platform/accessibility/browser/accessibleView.ts +++ b/src/vs/platform/accessibility/browser/accessibleView.ts @@ -120,6 +120,7 @@ export interface IAccessibleViewService { getOpenAriaHint(verbositySettingKey: string): string | null; getCodeBlockContext(): ICodeBlockActionContext | undefined; configureKeybindings(): void; + openHelpLink(): void; } diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts index 3cad6386092..d1a40cc9446 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts @@ -363,6 +363,13 @@ export class AccessibleView extends Disposable { return symbols.length ? symbols : undefined; } + openHelpLink(): void { + if (!this._currentProvider?.options.readMoreUrl) { + return; + } + this._openerService.open(URI.parse(this._currentProvider.options.readMoreUrl)); + } + configureKeybindings(): void { const items = this._currentProvider?.options?.configureKeybindingItems; const provider = this._currentProvider; @@ -470,10 +477,10 @@ export class AccessibleView extends Disposable { this._currentProvider = provider; this._accessibleViewCurrentProviderId.set(provider.id); const verbose = this._verbosityEnabled(); - const readMoreLink = provider.options.readMoreUrl ? localize("openDoc", "\n\nOpen a browser window with more information related to accessibility (H).") : ''; + const readMoreLink = provider.options.readMoreUrl ? localize("openDoc", "\n\nOpen a browser window with more information related to accessibility.", AccessibilityCommandId.AccessibilityHelpOpenHelpLink) : ''; let disableHelpHint = ''; if (provider instanceof AdvancedContentProvider && provider.options.type === AccessibleViewType.Help && verbose) { - disableHelpHint = this._getDisableVerbosityHint(provider.verbositySettingKey); + disableHelpHint = this._getDisableVerbosityHint(); } const accessibilitySupport = this._accessibilityService.isScreenReaderOptimized(); let message = ''; @@ -494,7 +501,7 @@ export class AccessibleView extends Disposable { const exitThisDialogHint = verbose && !provider.options.position ? localize('exit', '\n\nExit this dialog (Escape).') : ''; let content = provider.provideContent(); if (provider.options.type === AccessibleViewType.Help) { - const resolvedContent = resolveContentAndKeybindingItems(this._keybindingService, content); + const resolvedContent = resolveContentAndKeybindingItems(this._keybindingService, content + readMoreLink + disableHelpHint + exitThisDialogHint); if (resolvedContent) { content = resolvedContent.content.value; if (resolvedContent.configureKeybindingItems) { @@ -502,7 +509,7 @@ export class AccessibleView extends Disposable { } } } - const newContent = message + content + readMoreLink + disableHelpHint + exitThisDialogHint; + const newContent = message + content; this.calculateCodeBlocks(newContent); this._currentContent = newContent; this._updateContextKeys(provider, true); @@ -708,66 +715,24 @@ export class AccessibleView extends Disposable { if (this._currentProvider?.id !== AccessibleViewProviderId.Chat) { return; } - let hint = ''; - const insertAtCursorKb = this._keybindingService.lookupKeybinding('workbench.action.chat.insertCodeBlock')?.getAriaLabel(); - const insertIntoNewFileKb = this._keybindingService.lookupKeybinding('workbench.action.chat.insertIntoNewFile')?.getAriaLabel(); - const runInTerminalKb = this._keybindingService.lookupKeybinding('workbench.action.chat.runInTerminal')?.getAriaLabel(); - - if (insertAtCursorKb) { - hint += localize('insertAtCursor', " - Insert the code block at the cursor ({0}).\n", insertAtCursorKb); - } else { - hint += localize('insertAtCursorNoKb', " - Insert the code block at the cursor by configuring a keybinding for the Chat: Insert Code Block command.\n"); - } - if (insertIntoNewFileKb) { - hint += localize('insertIntoNewFile', " - Insert the code block into a new file ({0}).\n", insertIntoNewFileKb); - } else { - hint += localize('insertIntoNewFileNoKb', " - Insert the code block into a new file by configuring a keybinding for the Chat: Insert into New File command.\n"); - } - if (runInTerminalKb) { - hint += localize('runInTerminal', " - Run the code block in the terminal ({0}).\n", runInTerminalKb); - } else { - hint += localize('runInTerminalNoKb', " - Run the coe block in the terminal by configuring a keybinding for the Chat: Insert into Terminal command.\n"); - } - - return hint; + return [localize('insertAtCursor', " - Insert the code block at the cursor."), + localize('insertIntoNewFile', " - Insert the code block into a new file."), + localize('runInTerminal', " - Run the code block in the terminal.\n")].join('\n'); } private _getNavigationHint(): string { - let hint = ''; - const nextKeybinding = this._keybindingService.lookupKeybinding(AccessibilityCommandId.ShowNext)?.getAriaLabel(); - const previousKeybinding = this._keybindingService.lookupKeybinding(AccessibilityCommandId.ShowPrevious)?.getAriaLabel(); - if (nextKeybinding && previousKeybinding) { - hint = localize('accessibleViewNextPreviousHint', "Show the next ({0}) or previous ({1}) item.", nextKeybinding, previousKeybinding); - } else { - hint = localize('chatAccessibleViewNextPreviousHintNoKb', "Show the next or previous item by configuring keybindings for the Show Next & Previous in Accessible View commands."); - } - return hint; - } - private _getDisableVerbosityHint(verbositySettingKey: AccessibilityVerbositySettingId | string): string { - if (!this._configurationService.getValue(verbositySettingKey)) { - return ''; - } - let hint = ''; - const disableKeybinding = this._keybindingService.lookupKeybinding(AccessibilityCommandId.DisableVerbosityHint, this._contextKeyService)?.getAriaLabel(); - if (disableKeybinding) { - hint = localize('acessibleViewDisableHint', "\n\nDisable accessibility verbosity for this feature ({0}).", disableKeybinding); - } else { - hint = localize('accessibleViewDisableHintNoKb', "\n\nAdd a keybinding for the command Disable Accessible View Hint, which disables accessibility verbosity for this feature.s"); - } - return hint; + return localize('accessibleViewNextPreviousHint', "Show the next item or previous item.", AccessibilityCommandId.ShowNext, AccessibilityCommandId.ShowPrevious); } - private _getGoToSymbolHint(providerHasSymbols?: boolean): string { - const goToSymbolKb = this._keybindingService.lookupKeybinding(AccessibilityCommandId.GoToSymbol)?.getAriaLabel(); - let goToSymbolHint = ''; - if (providerHasSymbols) { - if (goToSymbolKb) { - goToSymbolHint = localize('goToSymbolHint', 'Go to a symbol ({0}).', goToSymbolKb); - } else { - goToSymbolHint = localize('goToSymbolHintNoKb', 'To go to a symbol, configure a keybinding for the command Go To Symbol in Accessible View'); - } + private _getDisableVerbosityHint(): string { + return localize('acessibleViewDisableHint', "\n\nDisable accessibility verbosity for this feature.", AccessibilityCommandId.DisableVerbosityHint); + } + + private _getGoToSymbolHint(providerHasSymbols?: boolean): string | undefined { + if (!providerHasSymbols) { + return; } - return goToSymbolHint; + return localize('goToSymbolHint', 'Go to a symbol.', AccessibilityCommandId.GoToSymbol); } } @@ -792,6 +757,9 @@ export class AccessibleViewService extends Disposable implements IAccessibleView configureKeybindings(): void { this._accessibleView?.configureKeybindings(); } + openHelpLink(): void { + this._accessibleView?.openHelpLink(); + } showLastProvider(id: AccessibleViewProviderId): void { this._accessibleView?.showLastProvider(id); } diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts index 3a437fcbc14..d4f6472c0c1 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts @@ -246,6 +246,25 @@ class AccessibilityHelpConfigureKeybindingsAction extends Action2 { } registerAction2(AccessibilityHelpConfigureKeybindingsAction); + +class AccessibilityHelpOpenHelpLinkAction extends Action2 { + constructor() { + super({ + id: AccessibilityCommandId.AccessibilityHelpOpenHelpLink, + precondition: ContextKeyExpr.and(accessibilityHelpIsShown), + keybinding: { + primary: KeyMod.Alt | KeyCode.KeyH, + weight: KeybindingWeight.WorkbenchContrib + }, + title: localize('editor.action.accessibilityHelpOpenHelpLink', "Accessibility Help Open Help Link") + }); + } + run(accessor: ServicesAccessor): void { + accessor.get(IAccessibleViewService).openHelpLink(); + } +} +registerAction2(AccessibilityHelpOpenHelpLinkAction); + class AccessibleViewAcceptInlineCompletionAction extends Action2 { constructor() { super({ diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleViewContributions.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleViewContributions.ts index 73b4e7ce169..d8d07767246 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleViewContributions.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleViewContributions.ts @@ -4,23 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable } from 'vs/base/common/lifecycle'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { accessibleViewIsShown } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import * as strings from 'vs/base/common/strings'; import { AccessibilityHelpAction, AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; import { AccessibleViewType, AdvancedContentProvider, ExtensionContentProvider, IAccessibleViewService } from 'vs/platform/accessibility/browser/accessibleView'; import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -export function descriptionForCommand(commandId: string, msg: string, noKbMsg: string, keybindingService: IKeybindingService): string { - const kb = keybindingService.lookupKeybinding(commandId); - if (kb) { - return strings.format(msg, kb.getAriaLabel()); - } - return strings.format(noKbMsg, commandId); -} - - export class AccesibleViewHelpContribution extends Disposable { static ID: 'accesibleViewHelpContribution'; constructor() { diff --git a/src/vs/workbench/contrib/accessibility/common/accessibilityCommands.ts b/src/vs/workbench/contrib/accessibility/common/accessibilityCommands.ts index 4c5e852c355..d689c02503e 100644 --- a/src/vs/workbench/contrib/accessibility/common/accessibilityCommands.ts +++ b/src/vs/workbench/contrib/accessibility/common/accessibilityCommands.ts @@ -14,4 +14,5 @@ export const enum AccessibilityCommandId { NextCodeBlock = 'editor.action.accessibleViewNextCodeBlock', PreviousCodeBlock = 'editor.action.accessibleViewPreviousCodeBlock', AccessibilityHelpConfigureKeybindings = 'editor.action.accessibilityHelpConfigureKeybindings', + AccessibilityHelpOpenHelpLink = 'editor.action.accessibilityHelpOpenHelpLink', } diff --git a/src/vs/workbench/contrib/codeEditor/browser/diffEditorAccessibilityHelp.ts b/src/vs/workbench/contrib/codeEditor/browser/diffEditorAccessibilityHelp.ts index dbc7ffb7a78..6ca47252ca5 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/diffEditorAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/diffEditorAccessibilityHelp.ts @@ -36,22 +36,13 @@ export class DiffEditorAccessibilityHelp implements IAccessibleViewImplentation return; } - const next = keybindingService.lookupKeybinding(AccessibleDiffViewerNext.id)?.getAriaLabel(); - const previous = keybindingService.lookupKeybinding(AccessibleDiffViewerPrev.id)?.getAriaLabel(); - let switchSides; - const switchSidesKb = keybindingService.lookupKeybinding('diffEditor.switchSide')?.getAriaLabel(); - if (switchSidesKb) { - switchSides = localize('msg3', "Run the command Diff Editor: Switch Side ({0}) to toggle between the original and modified editors.", switchSidesKb); - } else { - switchSides = localize('switchSidesNoKb', "Run the command Diff Editor: Switch Side, which is currently not triggerable via keybinding, to toggle between the original and modified editors."); - } - + const switchSides = localize('msg3', "Run the command Diff Editor: Switch Side to toggle between the original and modified editors."); const diffEditorActiveAnnouncement = localize('msg5', "The setting, accessibility.verbosity.diffEditorActive, controls if a diff editor announcement is made when it becomes the active editor."); const keys = ['accessibility.signals.diffLineDeleted', 'accessibility.signals.diffLineInserted', 'accessibility.signals.diffLineModified']; const content = [ localize('msg1', "You are in a diff editor."), - localize('msg2', "View the next ({0}) or previous ({1}) diff in diff review mode, which is optimized for screen readers.", next, previous), + localize('msg2', "View the next or previous diff in diff review mode, which is optimized for screen readers.", AccessibleDiffViewerNext.id, AccessibleDiffViewerPrev.id), switchSides, diffEditorActiveAnnouncement, localize('msg4', "To control which accessibility signals should be played, the following settings can be configured: {0}.", keys.join(', ')), diff --git a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibilityHelp.ts b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibilityHelp.ts index 25efa6cb270..cd33345cc89 100644 --- a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibilityHelp.ts @@ -4,13 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable } from 'vs/base/common/lifecycle'; -import { format } from 'vs/base/common/strings'; import { localize } from 'vs/nls'; -import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ShellIntegrationStatus, TerminalSettingId, WindowsShellType } from 'vs/platform/terminal/common/terminal'; import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; import { ITerminalInstance, IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal'; @@ -50,37 +47,16 @@ export class TerminalAccessibilityHelpProvider extends Disposable implements IAc private readonly _instance: Pick, _xterm: Pick & { raw: Terminal }, @IInstantiationService _instantiationService: IInstantiationService, - @IKeybindingService private readonly _keybindingService: IKeybindingService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @ICommandService private readonly _commandService: ICommandService, - @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, @IConfigurationService private readonly _configurationService: IConfigurationService ) { super(); this._hasShellIntegration = _xterm.shellIntegration.status === ShellIntegrationStatus.VSCode; } - - private _descriptionForCommand(commandId: string, msg: string, noKbMsg: string): string { - if (commandId === TerminalCommandId.RunRecentCommand) { - const kb = this._keybindingService.lookupKeybindings(commandId); - // Run recent command has multiple keybindings. lookupKeybinding just returns the first one regardless of the when context. - // Thus, we have to check if accessibility mode is enabled to determine which keybinding to use. - const isScreenReaderOptimized = this._accessibilityService.isScreenReaderOptimized(); - if (isScreenReaderOptimized && kb[1]) { - format(msg, kb[1].getAriaLabel()); - } else if (kb[0]) { - format(msg, kb[0].getAriaLabel()); - } else { - return format(noKbMsg, commandId); - } - } - const kb = this._keybindingService.lookupKeybinding(commandId, this._contextKeyService)?.getAriaLabel(); - return !kb ? format(noKbMsg, commandId) : format(msg, kb); - } - provideContent(): string { const content = []; - content.push(this._descriptionForCommand(TerminalAccessibilityCommandId.FocusAccessibleBuffer, localize('focusAccessibleTerminalView', 'The Focus Accessible Terminal View ({0}) command enables screen readers to read terminal contents.'), localize('focusAccessibleTerminalViewNoKb', 'The Focus Terminal Accessible View command enables screen readers to read terminal contents and is currently not triggerable by a keybinding.'))); + content.push(localize('focusAccessibleTerminalView', 'The Focus Accessible Terminal View command enables screen readers to read terminal contents.', TerminalAccessibilityCommandId.FocusAccessibleBuffer)); content.push(localize('preserveCursor', 'Customize the behavior of the cursor when toggling between the terminal and accessible view with `terminal.integrated.accessibleViewPreserveCursorPosition.`')); if (!this._configurationService.getValue(TerminalAccessibilitySettingId.AccessibleViewFocusOnCommandExecution)) { content.push(localize('focusViewOnExecution', 'Enable `terminal.integrated.accessibleViewFocusOnCommandExecution` to automatically focus the terminal accessible view when a command is executed in the terminal.')); @@ -91,17 +67,17 @@ export class TerminalAccessibilityHelpProvider extends Disposable implements IAc if (this._hasShellIntegration) { const shellIntegrationCommandList = []; shellIntegrationCommandList.push(localize('shellIntegration', "The terminal has a feature called shell integration that offers an enhanced experience and provides useful commands for screen readers such as:")); - shellIntegrationCommandList.push('- ' + this._descriptionForCommand(TerminalAccessibilityCommandId.AccessibleBufferGoToNextCommand, localize('goToNextCommand', 'Go to Next Command ({0}) in the accessible view'), localize('goToNextCommandNoKb', 'Go to Next Command in the accessible view is currently not triggerable by a keybinding.'))); - shellIntegrationCommandList.push('- ' + this._descriptionForCommand(TerminalAccessibilityCommandId.AccessibleBufferGoToPreviousCommand, localize('goToPreviousCommand', 'Go to Previous Command ({0}) in the accessible view'), localize('goToPreviousCommandNoKb', 'Go to Previous Command in the accessible view is currently not triggerable by a keybinding.'))); - shellIntegrationCommandList.push('- ' + this._descriptionForCommand(AccessibilityCommandId.GoToSymbol, localize('goToSymbol', 'Go to Symbol ({0})'), localize('goToSymbolNoKb', 'Go to symbol is currently not triggerable by a keybinding.'))); - shellIntegrationCommandList.push('- ' + this._descriptionForCommand(TerminalCommandId.RunRecentCommand, localize('runRecentCommand', 'Run Recent Command ({0})'), localize('runRecentCommandNoKb', 'Run Recent Command is currently not triggerable by a keybinding.'))); - shellIntegrationCommandList.push('- ' + this._descriptionForCommand(TerminalCommandId.GoToRecentDirectory, localize('goToRecentDirectory', 'Go to Recent Directory ({0})'), localize('goToRecentDirectoryNoKb', 'Go to Recent Directory is currently not triggerable by a keybinding.'))); + shellIntegrationCommandList.push('- ' + localize('goToNextCommand', 'Go to Next Command in the accessible view', TerminalAccessibilityCommandId.AccessibleBufferGoToNextCommand)); + shellIntegrationCommandList.push('- ' + localize('goToPreviousCommand', 'Go to Previous Command in the accessible view', TerminalAccessibilityCommandId.AccessibleBufferGoToPreviousCommand)); + shellIntegrationCommandList.push('- ' + localize('goToSymbol', 'Go to Symbol', AccessibilityCommandId.GoToSymbol)); + shellIntegrationCommandList.push('- ' + localize('runRecentCommand', 'Run Recent Command', TerminalCommandId.RunRecentCommand)); + shellIntegrationCommandList.push('- ' + localize('goToRecentDirectory', 'Go to Recent Directory', TerminalCommandId.GoToRecentDirectory)); content.push(shellIntegrationCommandList.join('\n')); } else { - content.push(this._descriptionForCommand(TerminalCommandId.RunRecentCommand, localize('goToRecentDirectoryNoShellIntegration', 'The Go to Recent Directory command ({0}) enables screen readers to easily navigate to a directory that has been used in the terminal.'), localize('goToRecentDirectoryNoKbNoShellIntegration', 'The Go to Recent Directory command enables screen readers to easily navigate to a directory that has been used in the terminal and is currently not triggerable by a keybinding.'))); + content.push(localize('goToRecentDirectoryNoShellIntegration', 'The Go to Recent Directory command enables screen readers to easily navigate to a directory that has been used in the terminal.', TerminalCommandId.RunRecentCommand)); } - content.push(this._descriptionForCommand(TerminalLinksCommandId.OpenDetectedLink, localize('openDetectedLink', 'The Open Detected Link ({0}) command enables screen readers to easily open links found in the terminal.'), localize('openDetectedLinkNoKb', 'The Open Detected Link command enables screen readers to easily open links found in the terminal and is currently not triggerable by a keybinding.'))); - content.push(this._descriptionForCommand(TerminalCommandId.NewWithProfile, localize('newWithProfile', 'The Create New Terminal (With Profile) ({0}) command allows for easy terminal creation using a specific profile.'), localize('newWithProfileNoKb', 'The Create New Terminal (With Profile) command allows for easy terminal creation using a specific profile and is currently not triggerable by a keybinding.'))); + content.push(localize('openDetectedLink', 'The Open Detected Link command enables screen readers to easily open links found in the terminal.', TerminalLinksCommandId.OpenDetectedLink)); + content.push(localize('newWithProfile', 'The Create New Terminal (With Profile) command allows for easy terminal creation using a specific profile.', TerminalCommandId.NewWithProfile)); content.push(localize('focusAfterRun', 'Configure what gets focused after running selected text in the terminal with `{0}`.', TerminalSettingId.FocusAfterRun)); return content.join('\n\n'); } From c0c8cddc34bc3372b54aad456b4609288b28b1d4 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 21 May 2024 13:58:45 -0700 Subject: [PATCH 299/357] Use `ChatAgentService` instead of `InlineChatService` for terminal chat hint (#213183) fix #213159 --- .../terminal.initialHint.contribution.ts | 31 ++++++---- .../test/browser/terminalInitialHint.test.ts | 60 ++++++++++++++----- 2 files changed, 63 insertions(+), 28 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts index 9d746a55b08..dbead33ec9a 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts @@ -21,7 +21,6 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { ICommandService } from 'vs/platform/commands/common/commands'; import { IProductService } from 'vs/platform/product/common/productService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IInlineChatService, IInlineChatSessionProvider, InlineChatProviderChangeEvent } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { status } from 'vs/base/browser/ui/aria/aria'; import * as dom from 'vs/base/browser/dom'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -29,6 +28,7 @@ import { TerminalChatCommandId } from 'vs/workbench/contrib/terminalContrib/chat import { TerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminalInstance'; import 'vs/css!./media/terminalInitialHint'; import { TerminalInitialHintSettingId } from 'vs/workbench/contrib/terminalContrib/chat/common/terminalInitialHintConfiguration'; +import { ChatAgentLocation, IChatAgent, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; const $ = dom.$; @@ -38,7 +38,7 @@ export class InitialHintAddon extends Disposable implements ITerminalAddon { private readonly _disposables = this._register(new MutableDisposable()); constructor(private readonly _capabilities: ITerminalCapabilityStore, - private readonly _onDidChangeProviders: Event) { + private readonly _onDidChangeAgents: Event) { super(); } activate(terminal: RawXtermTerminal): void { @@ -58,8 +58,13 @@ export class InitialHintAddon extends Disposable implements ITerminalAddon { } })); } - - this._disposables.value?.add(Event.once(this._onDidChangeProviders)(() => this._onDidRequestCreateHint.fire())); + const agentListener = this._onDidChangeAgents((e) => { + if (e?.locations.includes(ChatAgentLocation.Terminal)) { + this._onDidRequestCreateHint.fire(); + agentListener.dispose(); + } + }); + this._disposables.value?.add(agentListener); } } @@ -80,11 +85,11 @@ export class TerminalInitialHintContribution extends Disposable implements ITerm private readonly _instance: Pick | IDetachedTerminalInstance, processManager: ITerminalProcessManager | ITerminalProcessInfo | undefined, widgetManager: TerminalWidgetManager | undefined, - @IInlineChatService private readonly _inlineChatService: IInlineChatService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IConfigurationService private readonly _configurationService: IConfigurationService, @ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService, @ITerminalEditorService private readonly _terminalEditorService: ITerminalEditorService, + @IChatAgentService private readonly _chatAgentService: IChatAgentService, ) { super(); } @@ -95,7 +100,7 @@ export class TerminalInitialHintContribution extends Disposable implements ITerm return; } this._xterm = xterm; - this._addon = this._register(this._instantiationService.createInstance(InitialHintAddon, this._instance.capabilities, this._inlineChatService.onDidChangeProviders)); + this._addon = this._register(this._instantiationService.createInstance(InitialHintAddon, this._instance.capabilities, this._chatAgentService.onDidChangeAgents)); this._xterm.raw.loadAddon(this._addon); this._register(this._addon.onDidRequestCreateHint(() => this._createHint())); } @@ -148,11 +153,11 @@ export class TerminalInitialHintContribution extends Disposable implements ITerm this._register(this._decoration); this._register(this._decoration.onRender((e) => { if (!this._hintWidget && this._xterm?.isFocused && this._terminalGroupService.instances.length + this._terminalEditorService.instances.length === 1) { - const chatProviders = [...this._inlineChatService.getAllProvider()]; - if (chatProviders?.length) { + const terminalAgents = this._chatAgentService.getActivatedAgents().filter(candidate => candidate.locations.includes(ChatAgentLocation.Terminal)); + if (terminalAgents?.length) { const widget = this._register(this._instantiationService.createInstance(TerminalInitialHintWidget, instance)); this._addon?.dispose(); - this._hintWidget = widget.getDomNode(chatProviders); + this._hintWidget = widget.getDomNode(terminalAgents); if (!this._hintWidget) { return; } @@ -213,8 +218,8 @@ class TerminalInitialHintWidget extends Disposable { })); } - private _getHintInlineChat(providers: IInlineChatSessionProvider[]) { - const providerName = (providers.length === 1 ? providers[0].label : undefined) ?? this.productService.nameShort; + private _getHintInlineChat(agents: IChatAgent[]) { + const providerName = (agents.length === 1 ? agents[0].fullName : undefined) ?? this.productService.nameShort; let ariaLabel = `Ask ${providerName} something or start typing to dismiss.`; @@ -289,12 +294,12 @@ class TerminalInitialHintWidget extends Disposable { return { ariaLabel, hintHandler, hintElement }; } - getDomNode(providers: IInlineChatSessionProvider[]): HTMLElement { + getDomNode(agents: IChatAgent[]): HTMLElement { if (!this.domNode) { this.domNode = $('.terminal-initial-hint'); this.domNode!.style.paddingLeft = '4px'; - const { hintElement, ariaLabel } = this._getHintInlineChat(providers); + const { hintElement, ariaLabel } = this._getHintInlineChat(agents); this.domNode.append(hintElement); this.ariaLabel = ariaLabel.concat(localize('disableHint', ' Toggle {0} in settings to disable this hint.', AccessibilityVerbositySettingId.TerminalChat)); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/test/browser/terminalInitialHint.test.ts b/src/vs/workbench/contrib/terminalContrib/chat/test/browser/terminalInitialHint.test.ts index c92241f6741..d3a1a8a0b11 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/test/browser/terminalInitialHint.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/test/browser/terminalInitialHint.test.ts @@ -10,10 +10,10 @@ import { workbenchInstantiationService } from 'vs/workbench/test/browser/workben import { NullLogService } from 'vs/platform/log/common/log'; import { InitialHintAddon } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution'; import { getActiveDocument } from 'vs/base/browser/dom'; -import { IInlineChatSessionProvider, InlineChatProviderChangeEvent } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { Emitter } from 'vs/base/common/event'; import { strictEqual } from 'assert'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { ChatAgentLocation, IChatAgent } from 'vs/workbench/contrib/chat/common/chatAgents'; // Test TerminalInitialHintAddon @@ -22,13 +22,35 @@ suite('Terminal Initial Hint Addon', () => { let eventCount = 0; let xterm: Terminal; let initialHintAddon: InitialHintAddon; - const _onDidChangeProviders: Emitter = new Emitter(); - const onDidChangeProviders = _onDidChangeProviders.event; + const _onDidChangeAgents: Emitter = new Emitter(); + const onDidChangeAgents = _onDidChangeAgents.event; + const agent: IChatAgent = { + id: 'termminal', + name: 'terminal', + extensionId: new ExtensionIdentifier('test'), + extensionPublisherId: 'test', + extensionDisplayName: 'test', + metadata: {}, + slashCommands: [{ name: 'test', description: 'test' }], + locations: [ChatAgentLocation.fromRaw('terminal')], + invoke: async () => { return {}; } + }; + const editorAgent: IChatAgent = { + id: 'editor', + name: 'editor', + extensionId: new ExtensionIdentifier('test-editor'), + extensionPublisherId: 'test-editor', + extensionDisplayName: 'test-editor', + metadata: {}, + slashCommands: [{ name: 'test', description: 'test' }], + locations: [ChatAgentLocation.fromRaw('editor')], + invoke: async () => { return {}; } + }; setup(() => { const instantiationService = workbenchInstantiationService({}, store); xterm = store.add(new Terminal()); const shellIntegrationAddon = store.add(new ShellIntegrationAddon('', true, undefined, new NullLogService)); - initialHintAddon = store.add(instantiationService.createInstance(InitialHintAddon, shellIntegrationAddon.capabilities, onDidChangeProviders)); + initialHintAddon = store.add(instantiationService.createInstance(InitialHintAddon, shellIntegrationAddon.capabilities, onDidChangeAgents)); store.add(initialHintAddon.onDidRequestCreateHint(() => eventCount++)); const testContainer = document.createElement('div'); getActiveDocument().body.append(testContainer); @@ -44,24 +66,32 @@ suite('Terminal Initial Hint Addon', () => { xterm.focus(); strictEqual(eventCount, 0); }); - test('hint is shown when there is a chat provider', () => { + test('hint is not shown when there is just an editor agent', () => { eventCount = 0; - const provider: IInlineChatSessionProvider = { - extensionId: new ExtensionIdentifier('test'), - label: 'blahblah' - }; - _onDidChangeProviders.fire({ added: provider }); + _onDidChangeAgents.fire(editorAgent); xterm.focus(); + strictEqual(eventCount, 0); + }); + test('hint is shown when there is a terminal chat agent', () => { + eventCount = 0; + _onDidChangeAgents.fire(editorAgent); + xterm.focus(); + strictEqual(eventCount, 0); + _onDidChangeAgents.fire(agent); + strictEqual(eventCount, 1); + }); + test('hint is not shown again when another terminal chat agent is added if it has already shown', () => { + eventCount = 0; + _onDidChangeAgents.fire(agent); + xterm.focus(); + strictEqual(eventCount, 1); + _onDidChangeAgents.fire(agent); strictEqual(eventCount, 1); }); }); suite('Input', () => { test('hint is not shown when there has been input', () => { - const provider: IInlineChatSessionProvider = { - extensionId: new ExtensionIdentifier('test'), - label: 'blahblah' - }; - _onDidChangeProviders.fire({ added: provider }); + _onDidChangeAgents.fire(agent); xterm.writeln('data'); setTimeout(() => { xterm.focus(); From 4a24bcdb681cac3ad1f55394a3dcf09312d3329d Mon Sep 17 00:00:00 2001 From: David Dossett Date: Tue, 21 May 2024 14:31:08 -0700 Subject: [PATCH 300/357] Tweak confirmations pading --- .../contrib/chat/browser/media/chatConfirmationWidget.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/media/chatConfirmationWidget.css b/src/vs/workbench/contrib/chat/browser/media/chatConfirmationWidget.css index 48b493331b8..e244f077dd6 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatConfirmationWidget.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatConfirmationWidget.css @@ -7,7 +7,7 @@ border: 1px solid var(--vscode-chat-requestBorder); border-radius: 4px; margin-bottom: 16px; - padding: 12px 16px 16px; + padding: 8px 12px 12px; } .chat-confirmation-widget .chat-confirmation-widget-title { From 1c3d1945511f6fbce1caaa59da3325900cf70808 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Tue, 21 May 2024 14:52:59 -0700 Subject: [PATCH 301/357] Properly handle errors thrown from renderer while invoking the chat agent (#213080) * Properly handle errors thrown from renderer while invoking the chat agent If we throw out of ChatService, then the request can't be canceled and stays "generating" forever * Update snapshot --- .../contrib/chat/common/chatAgents.ts | 8 +- .../contrib/chat/common/chatServiceImpl.ts | 18 +++++ .../ChatService_sendRequest_fails.0.snap | 81 +++++++++++++++++++ .../chat/test/common/chatService.test.ts | 11 +++ 4 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index 989000af391..3068f8bbdf7 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -333,7 +333,7 @@ export class ChatAgentService implements IChatAgentService { async invokeAgent(id: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { const data = this._getAgentEntry(id); if (!data?.impl) { - throw new Error(`No activated agent with id ${id}`); + throw new Error(`No activated agent with id "${id}"`); } return await data.impl.invoke(request, progress, history, token); @@ -342,7 +342,7 @@ export class ChatAgentService implements IChatAgentService { async getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { const data = this._getAgentEntry(id); if (!data?.impl) { - throw new Error(`No activated agent with id ${id}`); + throw new Error(`No activated agent with id "${id}"`); } if (!data.impl?.provideFollowups) { @@ -527,5 +527,9 @@ export function reviveSerializedAgent(raw: ISerializableChatAgentData): IChatAge agent.extensionDisplayName = ''; } + if (!('extensionId' in agent)) { + agent.extensionId = new ExtensionIdentifier(''); + } + return revive(agent); } diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index a106ee69435..296677d5bf3 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -6,6 +6,7 @@ import { coalesce } from 'vs/base/common/arrays'; import { DeferredPromise } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { toErrorMessage } from 'vs/base/common/errorMessage'; import { ErrorNoTelemetry } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { MarkdownString } from 'vs/base/common/htmlContent'; @@ -656,6 +657,23 @@ export class ChatService extends Disposable implements IChatService { }); } } + } catch (err) { + const result = 'error'; + this.telemetryService.publicLog2('interactiveSessionProviderInvoked', { + timeToFirstProgress: undefined, + totalTime: undefined, + result, + requestType, + agent: agentPart?.agent.id ?? '', + slashCommand: agentSlashCommandPart ? agentSlashCommandPart.command.name : commandPart?.slashCommand.command, + chatSessionId: model.sessionId, + location + }); + const rawResult: IChatAgentResult = { errorDetails: { message: err.message } }; + model.setResponse(request, rawResult); + completeResponseCreated(); + this.trace('sendRequest', `Error while handling request: ${toErrorMessage(err)}`); + model.completeResponse(request); } finally { listener.dispose(); } diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap new file mode 100644 index 00000000000..a749780f183 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap @@ -0,0 +1,81 @@ +{ + requesterUsername: "test", + requesterAvatarIconUri: undefined, + responderUsername: "", + responderAvatarIconUri: undefined, + initialLocation: "panel", + welcomeMessage: undefined, + requests: [ + { + message: { + parts: [ + { + range: { + start: 0, + endExclusive: 28 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 29 + }, + agent: { + name: "ChatProviderWithUsedContext", + id: "ChatProviderWithUsedContext", + extensionId: { + value: "nullExtensionDescription", + _lower: "nullextensiondescription" + }, + extensionPublisherId: "", + publisherDisplayName: "", + extensionDisplayName: "", + locations: [ "panel" ], + metadata: { }, + slashCommands: [ ] + }, + kind: "agent" + }, + { + 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: { errorDetails: { message: "No activated agent with id \"ChatProviderWithUsedContext\"" } }, + followups: undefined, + isCanceled: false, + vote: undefined, + agent: { + name: "ChatProviderWithUsedContext", + id: "ChatProviderWithUsedContext", + extensionId: { + value: "nullExtensionDescription", + _lower: "nullextensiondescription" + }, + extensionPublisherId: "", + publisherDisplayName: "", + extensionDisplayName: "", + locations: [ "panel" ], + metadata: { }, + slashCommands: [ ] + }, + slashCommand: undefined, + usedContext: undefined, + contentReferences: [ ] + } + ] +} \ No newline at end of file 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 513f786bb59..1e9ad196f8b 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts @@ -130,6 +130,17 @@ suite('ChatService', () => { assert.strictEqual(model.getRequests()[0].response?.response.asString(), 'test response'); }); + test('sendRequest fails', async () => { + const testService = testDisposables.add(instantiationService.createInstance(ChatService)); + + const model = testDisposables.add(testService.startSession(ChatAgentLocation.Panel, CancellationToken.None)); + const response = await testService.sendRequest(model.sessionId, `@${chatAgentWithUsedContextId} test request`); + assert(response); + await response.responseCompletePromise; + + await assertSnapshot(model.toExport()); + }); + test('can serialize', async () => { testDisposables.add(chatAgentService.registerAgentImplementation(chatAgentWithUsedContextId, chatAgentWithUsedContext)); chatAgentService.updateAgent(chatAgentWithUsedContextId, { requester: { name: 'test' } }); From 5328a3f143db47ee44cc698712d66cde99a5d20f Mon Sep 17 00:00:00 2001 From: Aaron Munger Date: Tue, 21 May 2024 15:23:34 -0700 Subject: [PATCH 302/357] Save on EH diagnostics (#213171) * added trace logs * another trace message * fix test ctors --- src/vs/workbench/api/common/extHost.api.impl.ts | 2 +- .../api/common/extHostDocumentContentProviders.ts | 5 +++++ src/vs/workbench/api/common/extHostNotebook.ts | 13 ++++++++++++- .../api/test/browser/extHostNotebook.test.ts | 2 +- .../api/test/browser/extHostNotebookKernel.test.ts | 2 +- .../contrib/notebook/common/notebookEditorModel.ts | 6 +++--- .../workingCopy/common/storedFileWorkingCopy.ts | 10 ++++++++++ 7 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index a2cbad65c65..8c3aa184689 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -178,7 +178,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostDocuments = rpcProtocol.set(ExtHostContext.ExtHostDocuments, new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors)); const extHostDocumentContentProviders = rpcProtocol.set(ExtHostContext.ExtHostDocumentContentProviders, new ExtHostDocumentContentProvider(rpcProtocol, extHostDocumentsAndEditors, extHostLogService)); const extHostDocumentSaveParticipant = rpcProtocol.set(ExtHostContext.ExtHostDocumentSaveParticipant, new ExtHostDocumentSaveParticipant(extHostLogService, extHostDocuments, rpcProtocol.getProxy(MainContext.MainThreadBulkEdits))); - const extHostNotebook = rpcProtocol.set(ExtHostContext.ExtHostNotebook, new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors, extHostDocuments, extHostConsumerFileSystem, extHostSearch)); + const extHostNotebook = rpcProtocol.set(ExtHostContext.ExtHostNotebook, new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors, extHostDocuments, extHostConsumerFileSystem, extHostSearch, extHostLogService)); const extHostNotebookDocuments = rpcProtocol.set(ExtHostContext.ExtHostNotebookDocuments, new ExtHostNotebookDocuments(extHostNotebook)); const extHostNotebookEditors = rpcProtocol.set(ExtHostContext.ExtHostNotebookEditors, new ExtHostNotebookEditors(extHostLogService, extHostNotebook)); const extHostNotebookKernels = rpcProtocol.set(ExtHostContext.ExtHostNotebookKernels, new ExtHostNotebookKernels(rpcProtocol, initData, extHostNotebook, extHostCommands, extHostLogService)); diff --git a/src/vs/workbench/api/common/extHostDocumentContentProviders.ts b/src/vs/workbench/api/common/extHostDocumentContentProviders.ts index daca96f1913..9ea63aa8eb0 100644 --- a/src/vs/workbench/api/common/extHostDocumentContentProviders.ts +++ b/src/vs/workbench/api/common/extHostDocumentContentProviders.ts @@ -37,6 +37,11 @@ export class ExtHostDocumentContentProvider implements ExtHostDocumentContentPro throw new Error(`scheme '${scheme}' already registered`); } + this._logService.warn('TEST WARNING'); + this._logService.error('TEST ERROR'); + this._logService.info('TEST INFO'); + this._logService.trace('TEST TRACE'); + const handle = ExtHostDocumentContentProvider._handlePool++; this._documentContentProviders.set(handle, provider); diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index 29e72e73a07..0657c297678 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -37,6 +37,7 @@ import { CellSearchModel } from 'vs/workbench/contrib/search/common/cellSearchMo import { INotebookCellMatchNoModel, INotebookFileMatchNoModel, IRawClosedNotebookFileMatch, genericCellMatchesToTextSearchMatches } from 'vs/workbench/contrib/search/common/searchNotebookHelpers'; import { NotebookPriorityInfo } from 'vs/workbench/contrib/search/common/search'; import { globMatchesResource } from 'vs/workbench/services/editor/common/editorResolverService'; +import { ILogService } from 'vs/platform/log/common/log'; export class ExtHostNotebookController implements ExtHostNotebookShape { private static _notebookStatusBarItemProviderHandlePool: number = 0; @@ -78,7 +79,8 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { private _textDocumentsAndEditors: ExtHostDocumentsAndEditors, private _textDocuments: ExtHostDocuments, private _extHostFileSystem: IExtHostConsumerFileSystem, - private _extHostSearch: IExtHostSearch + private _extHostSearch: IExtHostSearch, + private _logService: ILogService ) { this._notebookProxy = mainContext.getProxy(MainContext.MainThreadNotebook); this._notebookDocumentsProxy = mainContext.getProxy(MainContext.MainThreadNotebookDocuments); @@ -314,6 +316,8 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { async $saveNotebook(handle: number, uriComponents: UriComponents, versionId: number, options: files.IWriteFileOptions, token: CancellationToken): Promise { const uri = URI.revive(uriComponents); const serializer = this._notebookSerializer.get(handle); + this.trace(`enter saveNotebook(versionId: ${versionId}, ${uri.toString()})`); + if (!serializer) { throw new Error('NO serializer found'); } @@ -357,7 +361,9 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { await this._validateWriteFile(uri, options); const bytes = await serializer.serializer.serializeNotebook(data, token); + this.trace(`serialized versionId: ${versionId} ${uri.toString()}`); await this._extHostFileSystem.value.writeFile(uri, bytes); + this.trace(`Finished write versionId: ${versionId} ${uri.toString()}`); const providerExtUri = this._extHostFileSystem.getFileSystemProviderExtUri(uri.scheme); const stat = await this._extHostFileSystem.value.stat(uri); @@ -375,6 +381,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { children: undefined }; + this.trace(`exit saveNotebook(versionId: ${versionId}, ${uri.toString()})`); return fileStats; } @@ -718,4 +725,8 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { extHostCommands.registerApiCommand(commandDataToNotebook); extHostCommands.registerApiCommand(commandNotebookToData); } + + private trace(msg: string): void { + this._logService.trace(`[Extension Host Notebook] ${msg}`); + } } diff --git a/src/vs/workbench/api/test/browser/extHostNotebook.test.ts b/src/vs/workbench/api/test/browser/extHostNotebook.test.ts index 806a6b685dd..5a7ed7e4e70 100644 --- a/src/vs/workbench/api/test/browser/extHostNotebook.test.ts +++ b/src/vs/workbench/api/test/browser/extHostNotebook.test.ts @@ -66,7 +66,7 @@ suite('NotebookCell#Document', function () { override onExtensionError(): boolean { return true; } - }), extHostDocumentsAndEditors, extHostDocuments, extHostConsumerFileSystem, extHostSearch); + }), extHostDocumentsAndEditors, extHostDocuments, extHostConsumerFileSystem, extHostSearch, new NullLogService()); extHostNotebookDocuments = new ExtHostNotebookDocuments(extHostNotebooks); const reg = extHostNotebooks.registerNotebookSerializer(nullExtensionDescription, 'test', new class extends mock() { }); diff --git a/src/vs/workbench/api/test/browser/extHostNotebookKernel.test.ts b/src/vs/workbench/api/test/browser/extHostNotebookKernel.test.ts index 8af3ece68d9..5a7e6f434c2 100644 --- a/src/vs/workbench/api/test/browser/extHostNotebookKernel.test.ts +++ b/src/vs/workbench/api/test/browser/extHostNotebookKernel.test.ts @@ -105,7 +105,7 @@ suite('NotebookKernel', function () { }); extHostConsumerFileSystem = new ExtHostConsumerFileSystem(rpcProtocol, new ExtHostFileSystemInfo()); extHostSearch = new ExtHostSearch(rpcProtocol, new URITransformerService(null), new NullLogService()); - extHostNotebooks = new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors, extHostDocuments, extHostConsumerFileSystem, extHostSearch); + extHostNotebooks = new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors, extHostDocuments, extHostConsumerFileSystem, extHostSearch, new NullLogService()); extHostNotebookDocuments = new ExtHostNotebookDocuments(extHostNotebooks); diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index 5974292f2e5..3c26eff229a 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -250,17 +250,17 @@ export class NotebookFileWorkingCopyModel extends Disposable implements IStoredF if (!token.isCancellationRequested) { type notebookSaveErrorData = { isRemote: boolean; - versionMismatch: boolean; + error: Error; }; type notebookSaveErrorClassification = { owner: 'amunger'; comment: 'Detect if we are having issues saving a notebook on the Extension Host'; isRemote: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Whether the save is happening on a remote file system' }; - versionMismatch: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'If the error was because of a version mismatch' }; + error: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Info about the error that occurred' }; }; this._telemetryService.publicLogError2('notebook/SaveError', { isRemote: this._notebookModel.uri.scheme === Schemas.vscodeRemote, - versionMismatch: error instanceof Error && error.message === 'Document version mismatch' + error: error }); } diff --git a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts index 2fd6bad674e..f6a173613ac 100644 --- a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts @@ -35,6 +35,16 @@ import { IMarkdownString } from 'vs/base/common/htmlContent'; */ export interface IStoredFileWorkingCopyModelFactory extends IFileWorkingCopyModelFactory { } +export async function createOptionalResult(callback: (token: CancellationToken) => Promise, token: CancellationToken): Promise { + const result = await callback(token); + if (result === undefined && token.isCancellationRequested) { + return undefined; + } + else { + return assertIsDefined(result); + } +} + /** * The underlying model of a stored file working copy provides some * methods for the stored file working copy to function. The model is From 5fe7fc55f4fdf8222b52c2125091da7d967b3dd7 Mon Sep 17 00:00:00 2001 From: Aaron Munger Date: Tue, 21 May 2024 15:47:54 -0700 Subject: [PATCH 303/357] remove test logs (#213188) --- .../workbench/api/common/extHostDocumentContentProviders.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/vs/workbench/api/common/extHostDocumentContentProviders.ts b/src/vs/workbench/api/common/extHostDocumentContentProviders.ts index 9ea63aa8eb0..daca96f1913 100644 --- a/src/vs/workbench/api/common/extHostDocumentContentProviders.ts +++ b/src/vs/workbench/api/common/extHostDocumentContentProviders.ts @@ -37,11 +37,6 @@ export class ExtHostDocumentContentProvider implements ExtHostDocumentContentPro throw new Error(`scheme '${scheme}' already registered`); } - this._logService.warn('TEST WARNING'); - this._logService.error('TEST ERROR'); - this._logService.info('TEST INFO'); - this._logService.trace('TEST TRACE'); - const handle = ExtHostDocumentContentProvider._handlePool++; this._documentContentProviders.set(handle, provider); From e1dfc911ce86f227cc9cfda3ac2ccc95017e5729 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 21 May 2024 16:09:08 -0700 Subject: [PATCH 304/357] testing: exploratory UI for followup actions This adds an API that extensions can use to contribute 'followup' actions around test messages. Here just a dummy hello world using copilot, but extensions could have any action here, such as actions to update snapshots if a test failed: ![](https://memes.peet.io/img/24-05-c1f3e073-a2da-4f16-a033-c8f7e5cd4864.png) Implemented using a simple provider API. --- .../src/extension.ts | 16 +++- .../src/failingDeepStrictEqualAssertFixer.ts | 18 ++-- .../api/browser/mainThreadTesting.ts | 7 +- .../workbench/api/common/extHost.api.impl.ts | 4 + .../workbench/api/common/extHost.protocol.ts | 15 +++- src/vs/workbench/api/common/extHostTesting.ts | 89 ++++++++++++++++++- .../contrib/testing/browser/media/testing.css | 42 +++++++++ .../testing/browser/testingOutputPeek.ts | 78 +++++++++++++++- .../contrib/testing/common/testService.ts | 27 ++++-- .../contrib/testing/common/testServiceImpl.ts | 30 ++++++- .../contrib/testing/common/testTypes.ts | 14 +++ .../vscode.proposed.testObserver.d.ts | 9 ++ 12 files changed, 315 insertions(+), 34 deletions(-) diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts index e1b2fd8b51e..d22cb023c67 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts @@ -25,7 +25,7 @@ const TEST_FILE_PATTERN = 'src/vs/**/*.{test,integrationTest}.ts'; const getWorkspaceFolderForTestFile = (uri: vscode.Uri) => (uri.path.endsWith('.test.ts') || uri.path.endsWith('.integrationTest.ts')) && - uri.path.includes('/src/vs/') + uri.path.includes('/src/vs/') ? vscode.workspace.getWorkspaceFolder(uri) : undefined; @@ -41,6 +41,18 @@ export async function activate(context: vscode.ExtensionContext) { const ctrl = vscode.tests.createTestController('selfhost-test-controller', 'VS Code Tests'); const fileChangedEmitter = new vscode.EventEmitter(); + // todo@connor4312: tidy this up and make it work + // context.subscriptions.push(vscode.tests.registerTestFollowupProvider({ + // async provideFollowup(result, test, taskIndex, messageIndex, token) { + // await new Promise(r => setTimeout(r, 2000)); + // return [{ + // title: '$(sparkle) Ask copilot for help', + // command: 'asdf' + // }]; + // }, + // })); + + ctrl.resolveHandler = async test => { if (!test) { context.subscriptions.push(await startWatchingWorkspace(ctrl, fileChangedEmitter)); @@ -62,7 +74,7 @@ export async function activate(context: vscode.ExtensionContext) { }); const createRunHandler = ( - runnerCtor: { new (folder: vscode.WorkspaceFolder): VSCodeTestRunner }, + runnerCtor: { new(folder: vscode.WorkspaceFolder): VSCodeTestRunner }, kind: vscode.TestRunProfileKind, args: string[] = [] ) => { diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/failingDeepStrictEqualAssertFixer.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/failingDeepStrictEqualAssertFixer.ts index bd2a35d7abf..17e65cbce50 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/failingDeepStrictEqualAssertFixer.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/failingDeepStrictEqualAssertFixer.ts @@ -71,8 +71,6 @@ export class FailingDeepStrictEqualAssertFixer { }, }) ); - - tests.testResults; } dispose() { @@ -99,15 +97,15 @@ const formatJsonValue = (value: unknown) => { context => (node: ts.Node) => { const visitor = (node: ts.Node): ts.Node => ts.isPropertyAssignment(node) && - ts.isStringLiteralLike(node.name) && - identifierLikeRe.test(node.name.text) + ts.isStringLiteralLike(node.name) && + identifierLikeRe.test(node.name.text) ? ts.factory.createPropertyAssignment( - ts.factory.createIdentifier(node.name.text), - ts.visitNode(node.initializer, visitor) as ts.Expression - ) + ts.factory.createIdentifier(node.name.text), + ts.visitNode(node.initializer, visitor) as ts.Expression + ) : ts.isStringLiteralLike(node) && node.text === '[undefined]' - ? ts.factory.createIdentifier('undefined') - : ts.visitEachChild(node, visitor, context); + ? ts.factory.createIdentifier('undefined') + : ts.visitEachChild(node, visitor, context); return ts.visitNode(node, visitor); }, @@ -190,7 +188,7 @@ class StrictEqualAssertion { return undefined; } - constructor(private readonly expression: ts.CallExpression) {} + constructor(private readonly expression: ts.CallExpression) { } /** Gets the expected value */ public get expectedValue(): ts.Expression | undefined { diff --git a/src/vs/workbench/api/browser/mainThreadTesting.ts b/src/vs/workbench/api/browser/mainThreadTesting.ts index e6e48c56bcb..0402df1b5fb 100644 --- a/src/vs/workbench/api/browser/mainThreadTesting.ts +++ b/src/vs/workbench/api/browser/mainThreadTesting.ts @@ -18,13 +18,13 @@ import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService'; import { LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; -import { IMainThreadTestController, ITestRootProvider, ITestService } from 'vs/workbench/contrib/testing/common/testService'; +import { IMainThreadTestController, ITestService } from 'vs/workbench/contrib/testing/common/testService'; import { CoverageDetails, ExtensionRunTestsRequest, IFileCoverage, ITestItem, ITestMessage, ITestRunProfile, ITestRunTask, ResolvedTestRunRequest, TestResultState, TestRunProfileBitset, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testTypes'; import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { ExtHostContext, ExtHostTestingShape, ILocationDto, ITestControllerPatch, MainContext, MainThreadTestingShape } from '../common/extHost.protocol'; @extHostNamedCustomer(MainContext.MainThreadTesting) -export class MainThreadTesting extends Disposable implements MainThreadTestingShape, ITestRootProvider { +export class MainThreadTesting extends Disposable implements MainThreadTestingShape { private readonly proxy: ExtHostTestingShape; private readonly diffListener = this._register(new MutableDisposable()); private readonly testProviderRegistrations = new Map this.proxy.$runControllerTests(reqs, token), startContinuousRun: (reqs, token) => this.proxy.$startContinuousRun(reqs, token), expandTest: (testId, levels) => this.proxy.$expandTest(testId, isFinite(levels) ? levels : -1), + provideTestFollowups: (req, token) => this.proxy.$provideTestFollowups(req, token), + executeTestFollowup: id => this.proxy.$executeTestFollowup(id), + disposeTestFollowups: ids => this.proxy.$disposeTestFollowups(ids), }; disposable.add(toDisposable(() => this.testProfiles.removeProfile(controllerId))); diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index a2cbad65c65..10ea7accc6b 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -453,6 +453,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'testObserver'); return extHostTesting.runTests(provider); }, + registerTestFollowupProvider(provider) { + checkProposedApiEnabled(extension, 'testObserver'); + return extHostTesting.registerTestFollowupProvider(provider); + }, get onDidChangeTestResults() { checkProposedApiEnabled(extension, 'testObserver'); return _asExtensionEvent(extHostTesting.onResultsChanged); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 2e0a39fb1a5..2d10d6b7129 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -65,7 +65,7 @@ import { InputValidationType } from 'vs/workbench/contrib/scm/common/scm'; import { IWorkspaceSymbol, NotebookPriorityInfo } from 'vs/workbench/contrib/search/common/search'; import { IRawClosedNotebookFileMatch } from 'vs/workbench/contrib/search/common/searchNotebookHelpers'; import { IKeywordRecognitionEvent, ISpeechProviderMetadata, ISpeechToTextEvent, ITextToSpeechEvent } from 'vs/workbench/contrib/speech/common/speechService'; -import { CoverageDetails, ExtensionRunTestsRequest, ICallProfileRunHandler, IFileCoverage, ISerializedTestResults, IStartControllerTests, ITestItem, ITestMessage, ITestRunProfile, ITestRunTask, ResolvedTestRunRequest, TestResultState, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testTypes'; +import { CoverageDetails, ExtensionRunTestsRequest, ICallProfileRunHandler, IFileCoverage, ISerializedTestResults, IStartControllerTests, ITestItem, ITestMessage, ITestRunProfile, ITestRunTask, ResolvedTestRunRequest, TestMessageFollowupRequest, TestMessageFollowupResponse, TestResultState, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testTypes'; import { Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor } from 'vs/workbench/contrib/timeline/common/timeline'; import { TypeHierarchyItem } from 'vs/workbench/contrib/typeHierarchy/common/typeHierarchy'; import { RelatedInformationResult, RelatedInformationType } from 'vs/workbench/services/aiRelatedInformation/common/aiRelatedInformation'; @@ -2703,8 +2703,6 @@ export interface ExtHostTestingShape { $cancelExtensionTestRun(runId: string | undefined): void; /** Handles a diff of tests, as a result of a subscribeToDiffs() call */ $acceptDiff(diff: TestsDiffOp.Serialized[]): void; - /** Publishes that a test run finished. */ - $publishTestResults(results: ISerializedTestResults[]): void; /** Expands a test item's children, by the given number of levels. */ $expandTest(testId: string, levels: number): Promise; /** Requests coverage details for a test run. Errors if not available. */ @@ -2719,6 +2717,17 @@ export interface ExtHostTestingShape { $syncTests(): Promise; /** Sets the active test run profiles */ $setDefaultRunProfiles(profiles: Record): void; + + // --- test results: + + /** Publishes that a test run finished. */ + $publishTestResults(results: ISerializedTestResults[]): void; + /** Requests followup actions for a test (failure) message */ + $provideTestFollowups(req: TestMessageFollowupRequest, token: CancellationToken): Promise; + /** Actions a followup actions for a test (failure) message */ + $executeTestFollowup(id: number): Promise; + /** Disposes followup actions for a test (failure) message */ + $disposeTestFollowups(id: number[]): void; } export interface ExtHostLocalizationShape { diff --git a/src/vs/workbench/api/common/extHostTesting.ts b/src/vs/workbench/api/common/extHostTesting.ts index 343cdc18143..64458c0b585 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/src/vs/workbench/api/common/extHostTesting.ts @@ -27,7 +27,7 @@ import { TestRunProfileKind, TestRunRequest, FileCoverage } from 'vs/workbench/a import { TestCommandId } from 'vs/workbench/contrib/testing/common/constants'; import { TestId, TestIdPathParts, TestPosition } from 'vs/workbench/contrib/testing/common/testId'; import { InvalidTestItemError } from 'vs/workbench/contrib/testing/common/testItemCollection'; -import { AbstractIncrementalTestCollection, CoverageDetails, ICallProfileRunHandler, ISerializedTestResults, IStartControllerTests, IStartControllerTestsResult, ITestErrorMessage, ITestItem, ITestItemContext, ITestMessageMenuArgs, ITestRunProfile, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, TestResultState, TestRunProfileBitset, TestsDiff, TestsDiffOp, isStartControllerTests } from 'vs/workbench/contrib/testing/common/testTypes'; +import { AbstractIncrementalTestCollection, CoverageDetails, ICallProfileRunHandler, ISerializedTestResults, IStartControllerTests, IStartControllerTestsResult, ITestErrorMessage, ITestItem, ITestItemContext, ITestMessageMenuArgs, ITestRunProfile, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, TestMessageFollowupRequest, TestMessageFollowupResponse, TestResultState, TestRunProfileBitset, TestsDiff, TestsDiffOp, isStartControllerTests } from 'vs/workbench/contrib/testing/common/testTypes'; import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import type * as vscode from 'vscode'; @@ -41,6 +41,10 @@ interface ControllerInfo { type DefaultProfileChangeEvent = Map>; +let followupCounter = 0; + +const testResultInternalIDs = new WeakMap(); + export class ExtHostTesting extends Disposable implements ExtHostTestingShape { private readonly resultsChangedEmitter = this._register(new Emitter()); protected readonly controllers = new Map(); @@ -48,14 +52,16 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { private readonly runTracker: TestRunCoordinator; private readonly observer: TestObservers; private readonly defaultProfilesChangedEmitter = this._register(new Emitter()); + private readonly followupProviders = new Set(); + private readonly testFollowups = new Map(); public onResultsChanged = this.resultsChangedEmitter.event; public results: ReadonlyArray = []; constructor( @IExtHostRpcService rpc: IExtHostRpcService, - @ILogService logService: ILogService, - commands: ExtHostCommands, + @ILogService private readonly logService: ILogService, + private readonly commands: ExtHostCommands, private readonly editors: ExtHostDocumentsAndEditors, ) { super(); @@ -222,6 +228,14 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { }, token); } + /** + * Implements vscode.test.registerTestFollowupProvider + */ + public registerTestFollowupProvider(provider: vscode.TestFollowupProvider): vscode.Disposable { + this.followupProviders.add(provider); + return { dispose: () => { this.followupProviders.delete(provider); } }; + } + /** * @inheritdoc */ @@ -292,7 +306,11 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { public $publishTestResults(results: ISerializedTestResults[]): void { this.results = Object.freeze( results - .map(Convert.TestResults.to) + .map(r => { + const o = Convert.TestResults.to(r); + testResultInternalIDs.set(o, r.id); + return o; + }) .concat(this.results) .sort((a, b) => b.completedAt - a.completedAt) .slice(0, 32), @@ -348,6 +366,52 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { return res; } + /** @inheritdoc */ + public async $provideTestFollowups(req: TestMessageFollowupRequest, token: CancellationToken): Promise { + const results = this.results.find(r => testResultInternalIDs.get(r) === req.resultId); + const test = results && findTestInResultSnapshot(TestId.fromString(req.extId), results?.results); + if (!test) { + return []; + } + + let followups: vscode.Command[] = []; + await Promise.all([...this.followupProviders].map(async provider => { + try { + const r = await provider.provideFollowup(results, test, req.taskIndex, req.messageIndex, token); + if (r) { + followups = followups.concat(r); + } + } catch (e) { + this.logService.error(`Error thrown while providing followup for test message`, e); + } + })); + + if (token.isCancellationRequested) { + return []; + } + + return followups.map(command => { + const id = followupCounter++; + this.testFollowups.set(id, command); + return { title: command.title, id }; + }); + } + + $disposeTestFollowups(id: number[]): void { + for (const i of id) { + this.testFollowups.delete(i); + } + } + + $executeTestFollowup(id: number): Promise { + const command = this.testFollowups.get(id); + if (!command) { + return Promise.resolve(); + } + + return this.commands.executeCommand(command.command, ...(command.arguments || [])); + } + private async runControllerTestRequest(req: ICallProfileRunHandler | ICallProfileRunHandler, isContinuous: boolean, token: CancellationToken): Promise { const lookup = this.controllers.get(req.controllerId); if (!lookup) { @@ -1202,3 +1266,20 @@ const profileGroupToBitset: { [K in TestRunProfileKind]: TestRunProfileBitset } [TestRunProfileKind.Debug]: TestRunProfileBitset.Debug, [TestRunProfileKind.Run]: TestRunProfileBitset.Run, }; + +function findTestInResultSnapshot(extId: TestId, snapshot: readonly Readonly[]) { + for (let i = 0; i < extId.path.length; i++) { + const item = snapshot.find(s => s.id === extId.path[i]); + if (!item) { + return undefined; + } + + if (i === extId.path.length - 1) { + return item; + } + + snapshot = item.children; + } + + return undefined; +} diff --git a/src/vs/workbench/contrib/testing/browser/media/testing.css b/src/vs/workbench/contrib/testing/browser/media/testing.css index c08621d410b..e37cf2c6815 100644 --- a/src/vs/workbench/contrib/testing/browser/media/testing.css +++ b/src/vs/workbench/contrib/testing/browser/media/testing.css @@ -267,6 +267,48 @@ cursor: pointer; } +.testing-followup-action { + position: absolute; + top: 100%; + left: 22px; + right: 22px; + margin-top: -25px; + line-height: 25px; + overflow: hidden; + pointer-events: none; + background: linear-gradient(transparent, var(--vscode-peekViewEditor-background) 50%); + + &.animated { + animation: fadeIn 150ms ease-out; + } + + > a { + display: flex; + align-items: center; + gap: 4px; + cursor: pointer; + pointer-events: auto; + width: fit-content; + + &, .codicon { + color: var(--vscode-textLink-foreground); + } + + &:hover { + color: var(--vscode-textLink-activeForeground); + } + + &[aria-disabled="true"] { + color: inherit; + cursor: default; + + .codicon { + color: inherit; + } + } + } +} + /** -- filter */ .monaco-action-bar.testing-filter-action-bar { flex-shrink: 0; diff --git a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts index 2bd0e5f3243..afbb0811a1c 100644 --- a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { alert } from 'vs/base/browser/ui/aria/aria'; import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; @@ -16,6 +17,7 @@ import { ITreeContextMenuEvent, ITreeNode } from 'vs/base/browser/ui/tree/tree'; import { Action, IAction, Separator } from 'vs/base/common/actions'; import { Delayer, Limiter, RunOnceScheduler } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Codicon } from 'vs/base/common/codicons'; import { Color } from 'vs/base/common/color'; import { Emitter, Event } from 'vs/base/common/event'; @@ -97,7 +99,7 @@ import { ITestExplorerFilterState } from 'vs/workbench/contrib/testing/common/te import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService'; import { ITaskRawOutput, ITestResult, ITestRunTaskResults, LiveTestResult, TestResultItemChange, TestResultItemChangeReason, maxCountPriority, resultItemParents } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultService, ResultChangeEvent } from 'vs/workbench/contrib/testing/common/testResultService'; -import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; +import { ITestFollowup, ITestService } from 'vs/workbench/contrib/testing/common/testService'; import { IRichLocation, ITestErrorMessage, ITestItem, ITestItemContext, ITestMessage, ITestMessageMenuArgs, ITestRunTask, ITestTaskState, InternalTestItem, TestMessageType, TestResultItem, TestResultState, TestRunProfileBitset, getMarkId, testResultStateToContextValues } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; import { IShowResultOptions, ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener'; @@ -769,11 +771,82 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo } } +const FOLLOWUP_ANIMATION_MIN_TIME = 500; + +class FollowupActionWidget extends Disposable { + private readonly el = dom.h('div.testing-followup-action', []); + private readonly visibleStore = this._register(new DisposableStore()); + + constructor( + private readonly container: HTMLElement, + @ITestService private readonly testService: ITestService, + ) { + super(); + } + + public show(subject: InspectSubject) { + this.visibleStore.clear(); + if (subject instanceof MessageSubject) { + this.showMessage(subject); + } + } + + private async showMessage(subject: MessageSubject) { + const cts = this.visibleStore.add(new CancellationTokenSource()); + const start = Date.now(); + const followups = await this.testService.provideTestFollowups({ + extId: subject.test.extId, + messageIndex: subject.messageIndex, + resultId: subject.result.id, + taskIndex: subject.taskIndex, + }, cts.token); + + + if (!followups.followups.length || cts.token.isCancellationRequested) { + followups.dispose(); + return; + } + + this.visibleStore.add(followups); + + dom.clearNode(this.el.root); + this.el.root.classList.toggle('animated', Date.now() - start > FOLLOWUP_ANIMATION_MIN_TIME); + for (const fu of followups.followups) { + const link = document.createElement('a'); + link.tabIndex = 0; + dom.reset(link, ...renderLabelWithIcons(fu.message)); + + this.visibleStore.add(dom.addDisposableListener(link, 'click', () => this.actionFollowup(link, fu))); + this.visibleStore.add(dom.addDisposableListener(link, 'keydown', e => { + const event = new StandardKeyboardEvent(e); + if (event.equals(KeyCode.Space) || event.equals(KeyCode.Enter)) { + this.actionFollowup(link, fu); + } + })); + + this.el.root.appendChild(link); + } + + this.container.appendChild(this.el.root); + this.visibleStore.add(toDisposable(() => { + this.el.root.parentElement?.removeChild(this.el.root); + })); + } + + private actionFollowup(link: HTMLAnchorElement, fu: ITestFollowup) { + if (link.ariaDisabled !== 'true') { + link.ariaDisabled = 'true'; + fu.execute(); + } + } +} + class TestResultsViewContent extends Disposable { private static lastSplitWidth?: number; private readonly didReveal = this._register(new Emitter<{ subject: InspectSubject; preserveFocus: boolean }>()); private readonly currentSubjectStore = this._register(new DisposableStore()); + private followupWidget!: FollowupActionWidget; private messageContextKeyService!: IContextKeyService; private contextKeyTestMessage!: IContextKey; private contextKeyResultOutdated!: IContextKey; @@ -810,6 +883,7 @@ class TestResultsViewContent extends Disposable { const { historyVisible, showRevealLocationOnMessages } = this.options; const isInPeekView = this.editor !== undefined; const messageContainer = this.messageContainer = dom.append(containerElement, dom.$('.test-output-peek-message-container')); + this.followupWidget = this._register(this.instantiationService.createInstance(FollowupActionWidget, messageContainer)); this.contentProviders = [ this._register(this.instantiationService.createInstance(DiffContentProvider, this.editor, messageContainer)), this._register(this.instantiationService.createInstance(MarkdownTestMessagePeek, messageContainer)), @@ -883,7 +957,7 @@ class TestResultsViewContent extends Disposable { this.current = opts.subject; return this.contentProvidersUpdateLimiter.queue(async () => { await Promise.all(this.contentProviders.map(p => p.update(opts.subject))); - + this.followupWidget.show(opts.subject); this.currentSubjectStore.clear(); this.populateFloatingClick(opts.subject); }); diff --git a/src/vs/workbench/contrib/testing/common/testService.ts b/src/vs/workbench/contrib/testing/common/testService.ts index 7af1ee2c331..09f008fa4fe 100644 --- a/src/vs/workbench/contrib/testing/common/testService.ts +++ b/src/vs/workbench/contrib/testing/common/testService.ts @@ -12,7 +12,7 @@ import { URI } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IObservableValue, MutableObservableValue } from 'vs/workbench/contrib/testing/common/observableValue'; -import { AbstractIncrementalTestCollection, ICallProfileRunHandler, IncrementalTestCollectionItem, InternalTestItem, ITestItemContext, ResolvedTestRunRequest, IStartControllerTests, IStartControllerTestsResult, TestItemExpandState, TestRunProfileBitset, TestsDiff } from 'vs/workbench/contrib/testing/common/testTypes'; +import { AbstractIncrementalTestCollection, ICallProfileRunHandler, IncrementalTestCollectionItem, InternalTestItem, ITestItemContext, ResolvedTestRunRequest, IStartControllerTests, IStartControllerTestsResult, TestItemExpandState, TestRunProfileBitset, TestsDiff, TestMessageFollowupResponse, TestMessageFollowupRequest } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestExclusions } from 'vs/workbench/contrib/testing/common/testExclusions'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { ITestResult } from 'vs/workbench/contrib/testing/common/testResult'; @@ -29,6 +29,9 @@ export interface IMainThreadTestController { expandTest(id: string, levels: number): Promise; startContinuousRun(request: ICallProfileRunHandler[], token: CancellationToken): Promise; runTests(request: IStartControllerTests[], token: CancellationToken): Promise; + provideTestFollowups(req: TestMessageFollowupRequest, token: CancellationToken): Promise; + executeTestFollowup(id: number): Promise; + disposeTestFollowups(ids: number[]): void; } export interface IMainThreadTestCollection extends AbstractIncrementalTestCollection { @@ -213,14 +216,6 @@ export const testsUnderUri = async function* (testService: ITestService, ident: } }; -/** - * An instance of the RootProvider should be registered for each extension - * host. - */ -export interface ITestRootProvider { - // todo: nothing, yet -} - /** * A run request that expresses the intent of the request and allows the * test service to resolve the specifics of the group. @@ -236,6 +231,15 @@ export interface AmbiguousRunTestsRequest { continuous?: boolean; } +export interface ITestFollowup { + message: string; + execute(): Promise; +} + +export interface ITestFollowups extends IDisposable { + followups: ITestFollowup[]; +} + export interface ITestService { readonly _serviceBrand: undefined; /** @@ -304,6 +308,11 @@ export interface ITestService { */ runResolvedTests(req: ResolvedTestRunRequest, token?: CancellationToken): Promise; + /** + * Provides followup actions for a test run. + */ + provideTestFollowups(req: TestMessageFollowupRequest, token: CancellationToken): Promise; + /** * Ensures the test diff from the remote ext host is flushed and waits for * any "busy" tests to become idle before resolving. diff --git a/src/vs/workbench/contrib/testing/common/testServiceImpl.ts b/src/vs/workbench/contrib/testing/common/testServiceImpl.ts index 04692c5dc81..2c34de5858e 100644 --- a/src/vs/workbench/contrib/testing/common/testServiceImpl.ts +++ b/src/vs/workbench/contrib/testing/common/testServiceImpl.ts @@ -27,8 +27,8 @@ import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingC import { canUseProfileWithTest, ITestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService'; import { ITestResult } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; -import { AmbiguousRunTestsRequest, IMainThreadTestController, ITestService } from 'vs/workbench/contrib/testing/common/testService'; -import { ResolvedTestRunRequest, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testTypes'; +import { AmbiguousRunTestsRequest, IMainThreadTestController, ITestFollowups, ITestService } from 'vs/workbench/contrib/testing/common/testService'; +import { ResolvedTestRunRequest, TestDiffOpType, TestMessageFollowupRequest, TestsDiff } from 'vs/workbench/contrib/testing/common/testTypes'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; export class TestService extends Disposable implements ITestService { @@ -264,6 +264,32 @@ export class TestService extends Disposable implements ITestService { } } + /** + * @inheritdoc + */ + public async provideTestFollowups(req: TestMessageFollowupRequest, token: CancellationToken): Promise { + const reqs = await Promise.all([...this.testControllers.values()] + .map(async ctrl => ({ ctrl, followups: await ctrl.provideTestFollowups(req, token) }))); + + const followups: ITestFollowups = { + followups: reqs.flatMap(({ ctrl, followups }) => followups.map(f => ({ + message: f.title, + execute: () => ctrl.executeTestFollowup(f.id) + }))), + dispose: () => { + for (const { ctrl, followups } of reqs) { + ctrl.disposeTestFollowups(followups.map(f => f.id)); + } + } + }; + + if (token.isCancellationRequested) { + followups.dispose(); + } + + return followups; + } + /** * @inheritdoc */ diff --git a/src/vs/workbench/contrib/testing/common/testTypes.ts b/src/vs/workbench/contrib/testing/common/testTypes.ts index d1f779a475d..75f7b371362 100644 --- a/src/vs/workbench/contrib/testing/common/testTypes.ts +++ b/src/vs/workbench/contrib/testing/common/testTypes.ts @@ -474,6 +474,20 @@ export const applyTestItemUpdate = (internal: InternalTestItem | ITestItemUpdate } }; +/** Request to an ext host to get followup messages for a test failure. */ +export interface TestMessageFollowupRequest { + resultId: string; + extId: string; + taskIndex: number; + messageIndex: number; +} + +/** Request to an ext host to get followup messages for a test failure. */ +export interface TestMessageFollowupResponse { + id: number; + title: string; +} + /** * Test result item used in the main thread. */ diff --git a/src/vscode-dts/vscode.proposed.testObserver.d.ts b/src/vscode-dts/vscode.proposed.testObserver.d.ts index d4465affbf2..cb8091c887e 100644 --- a/src/vscode-dts/vscode.proposed.testObserver.d.ts +++ b/src/vscode-dts/vscode.proposed.testObserver.d.ts @@ -15,6 +15,11 @@ declare module 'vscode' { */ export function runTests(run: TestRunRequest, token?: CancellationToken): Thenable; + /** + * Registers a provider that can provide follow-up actions for a test failure. + */ + export function registerTestFollowupProvider(provider: TestFollowupProvider): Disposable; + /** * Returns an observer that watches and can request tests. */ @@ -31,6 +36,10 @@ declare module 'vscode' { export const onDidChangeTestResults: Event; } + export interface TestFollowupProvider { + provideFollowup(result: TestRunResult, test: TestResultSnapshot, taskIndex: number, messageIndex: number, token: CancellationToken): ProviderResult; + } + export interface TestObserver { /** * List of tests returned by test provider for files in the workspace. From 0ba6e185dee5b2700f7a149189b63a63971193b1 Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Tue, 21 May 2024 16:54:11 -0700 Subject: [PATCH 305/357] Notebook Chat Controller does not need use agent directly --- .../controller/chat/notebookChatController.ts | 176 +++++++----------- 1 file changed, 72 insertions(+), 104 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts index 5e23e2e8b5f..2faeb9876bc 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts @@ -11,6 +11,7 @@ import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs import { LRUCache } from 'vs/base/common/map'; import { Schemas } from 'vs/base/common/network'; import { MovingAverage } from 'vs/base/common/numbers'; +import { isEqual } from 'vs/base/common/resources'; import { StopWatch } from 'vs/base/common/stopwatch'; import { assertType } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; @@ -28,11 +29,10 @@ import { localize } from 'vs/nls'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { GeneratingPhrase } from 'vs/workbench/contrib/chat/browser/chat'; -import { ChatAgentLocation, IChatAgentRequest, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { ChatModel, ChatRequestModel, getHistoryEntriesFromModel, IChatRequestVariableData, IChatResponseModel } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { IChatProgress, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; +import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; import { ProgressingEditsOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatStrategies'; import { InlineChatWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; @@ -267,9 +267,7 @@ export class NotebookChatController extends Disposable implements INotebookEdito private _focusTracker: IFocusTracker | undefined; private _widget: NotebookChatWidget | undefined; - private _notebookDefaultAgentId: string | undefined; private readonly _model: MutableDisposable = this._register(new MutableDisposable()); - private _currentRequest: ChatRequestModel | undefined; constructor( private readonly _notebookEditor: INotebookEditor, @IInstantiationService private readonly _instantiationService: IInstantiationService, @@ -279,9 +277,8 @@ export class NotebookChatController extends Disposable implements INotebookEdito @ILanguageService private readonly _languageService: ILanguageService, @INotebookExecutionStateService private _executionStateService: INotebookExecutionStateService, @IStorageService private readonly _storageService: IStorageService, - @IChatAgentService private readonly _chatAgentService: IChatAgentService, @IChatService private readonly _chatService: IChatService, - + @IChatVariablesService private readonly _chatVariableService: IChatVariablesService, ) { super(); this._ctxHasActiveRequest = CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST.bindTo(this._contextKeyService); @@ -303,19 +300,12 @@ export class NotebookChatController extends Disposable implements INotebookEdito this._storageService.store(NotebookChatController._storageKey, JSON.stringify(NotebookChatController._promptHistory), StorageScope.PROFILE, StorageTarget.USER); }; - if (!this._initNotebookAgent()) { - this._register(this._chatAgentService.onDidChangeAgents(() => this._initNotebookAgent())); - } - } - - private _initNotebookAgent(): boolean { - const notebookAgent = this._chatAgentService.getContributedDefaultAgent(ChatAgentLocation.Notebook); - if (notebookAgent) { - this._notebookDefaultAgentId = notebookAgent.id; - return true; - } - - return false; + this._register(this._chatVariableService.registerVariable( + { id: '_notebookChatInput', name: '_notebookChatInput', description: '', hidden: true }, + async (_message, _arg, model) => { + return this._widget?.parentEditor.getModel()?.uri; + } + )); } private _registerFocusTracker() { @@ -436,7 +426,13 @@ export class NotebookChatController extends Disposable implements INotebookEdito inputMenuId: MENU_CELL_CHAT_INPUT, widgetMenuId: MENU_CELL_CHAT_WIDGET, statusMenuId: MENU_CELL_CHAT_WIDGET_STATUS, - feedbackMenuId: MENU_CELL_CHAT_WIDGET_FEEDBACK + feedbackMenuId: MENU_CELL_CHAT_WIDGET_FEEDBACK, + rendererOptions: { + renderTextEditsAsSummary: (uri) => { + return isEqual(uri, this._widget?.parentEditor.getModel()?.uri) + || isEqual(uri, this._notebookEditor.textModel?.uri); + } + } } )); inlineChatWidget.placeholder = localize('default.placeholder', "Ask a question"); @@ -577,92 +573,68 @@ export class NotebookChatController extends Disposable implements INotebookEdito this._strategy = new EditStrategy(); - const model = this._model.value; - this._widget.inlineChatWidget.setChatModel(model); - - const request: IParsedChatRequest = { - text: lastInput, - parts: [] - }; - - const requestVarData: IChatRequestVariableData = { - variables: [] - }; - - this._currentRequest = model.addRequest(request, requestVarData, 0); - const responseCreated = new DeferredPromise(); - let responseCreatedComplete = false; - const completeResponseCreated = () => { - if (!responseCreatedComplete && this._currentRequest?.response) { - responseCreated.complete(this._currentRequest.response); - responseCreatedComplete = true; - } - }; - this._activeRequestCts?.cancel(); this._activeRequestCts = new CancellationTokenSource(); - const cancellationToken = new CancellationTokenSource().token; - const progressiveEditsQueue = new Queue(); - const progressiveEditsClock = StopWatch.create(); - const progressiveEditsAvgDuration = new MovingAverage(); - const progressiveEditsCts = new CancellationTokenSource(this._activeRequestCts.token); - const progressCallback = (progress: IChatProgress) => { - if (cancellationToken.isCancellationRequested) { - return; - } + const store = new DisposableStore(); - if (this._currentRequest) { - if (progress.kind === 'textEdit') { + try { + this._ctxHasActiveRequest.set(true); + + const progressiveEditsQueue = new Queue(); + const progressiveEditsClock = StopWatch.create(); + const progressiveEditsAvgDuration = new MovingAverage(); + const progressiveEditsCts = new CancellationTokenSource(this._activeRequestCts.token); + + const responsePromise = new DeferredPromise(); + const response = await this._widget.inlineChatWidget.chatWidget.acceptInput(); + if (response) { + let lastLength = 0; + + store.add(response.onDidChange(e => { + if (response.isCanceled) { + progressiveEditsCts.cancel(); + responsePromise.complete(); + return; + } + + if (response.isComplete) { + responsePromise.complete(); + return; + } + + const edits = response.response.value.map(part => { + if (part.kind === 'textEditGroup' + // && isEqual(part.uri, this._session?.textModelN.uri) + ) { + return part.edits; + } else { + return []; + } + }).flat(); + + const newEdits = edits.slice(lastLength); + // console.log('NEW edits', newEdits, edits); + if (newEdits.length === 0) { + return; // NO change + } + lastLength = edits.length; progressiveEditsAvgDuration.update(progressiveEditsClock.elapsed()); progressiveEditsClock.reset(); progressiveEditsQueue.queue(async () => { - // making changes goes into a queue because otherwise the async-progress time will - // influence the time it takes to receive the changes and progressive typing will - // become infinitely fast - - await this._makeChanges(progress.edits!, false - ? undefined - : { duration: progressiveEditsAvgDuration.value, token: progressiveEditsCts.token } - ); + for (const edits of newEdits) { + await this._makeChanges(edits, { + duration: progressiveEditsAvgDuration.value, + token: progressiveEditsCts.token + }); + } }); - } else { - model.acceptResponseProgress(this._currentRequest, progress); - } - completeResponseCreated(); + })); } - }; - await model.waitForInitialization(); - this._widget.inlineChatWidget.addToHistory(lastInput); - - completeResponseCreated(); - - const agentId = this._widget.inlineChatWidget.chatWidget.lastSelectedAgent ? this._widget.inlineChatWidget.chatWidget.lastSelectedAgent.id : this._notebookDefaultAgentId!; - const requestProps: IChatAgentRequest = { - sessionId: model.sessionId, - requestId: this._currentRequest!.id, - agentId: agentId, - message: lastInput, - variables: { - variables: [{ - id: '_notebookChatInput', - name: '_notebookChatInput', - value: this._widget.parentEditor.getModel()!.uri, - }] - }, - location: ChatAgentLocation.Notebook - }; - try { - this._ctxHasActiveRequest.set(true); - - const task = this._chatAgentService.invokeAgent(agentId, requestProps, progressCallback, getHistoryEntriesFromModel(model, agentId), cancellationToken); - this._widget.inlineChatWidget.updateChatMessage(undefined); - this._widget.inlineChatWidget.updateFollowUps(undefined); - this._widget.inlineChatWidget.updateProgress(true); - this._widget.inlineChatWidget.updateInfo(GeneratingPhrase + '\u2026'); - await task; + await responsePromise.p; + await progressiveEditsQueue.whenIdle(); this._userEditingDisposables.clear(); // monitor user edits @@ -681,17 +653,13 @@ export class NotebookChatController extends Disposable implements INotebookEdito } } catch (e) { } finally { + store.dispose(); + this._ctxHasActiveRequest.set(false); this._widget.inlineChatWidget.updateProgress(false); this._widget.inlineChatWidget.updateInfo(''); this._widget.inlineChatWidget.updateToolbar(true); - if (this._currentRequest) { - model.completeResponse(this._currentRequest); - completeResponseCreated(); - } } - - return responseCreated.p; } private async _makeChanges(edits: TextEdit[], opts: ProgressingEditsOptions | undefined) { From 692ab6d9c96d78744e8f1919106443672aa116c1 Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Tue, 21 May 2024 16:54:20 -0700 Subject: [PATCH 306/357] Hack to get it working --- src/vs/workbench/contrib/chat/common/chatServiceImpl.ts | 2 +- .../contrib/inlineChat/browser/inlineChatWidget.ts | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index a106ee69435..bc0d34f3ca1 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -568,7 +568,7 @@ export class ChatService extends Disposable implements IChatService { const updatedVariableData = updateRanges(variableData, promptTextResult.diff); // TODO bit of a hack // TODO- should figure out how to get rid of implicit variables for inline chat - const implicitVariablesEnabled = location === ChatAgentLocation.Editor; + const implicitVariablesEnabled = (location === ChatAgentLocation.Editor || location === ChatAgentLocation.Notebook); if (implicitVariablesEnabled) { const implicitVariables = agent.defaultImplicitVariables; if (implicitVariables) { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index e4cfccd30bc..f31b92040df 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -49,6 +49,7 @@ import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateF import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IHoverService } from 'vs/platform/hover/browser/hover'; import { IChatListItemRendererOptions } from 'vs/workbench/contrib/chat/browser/chat'; +import { CancellationToken } from 'vs/base/common/cancellation'; export interface InlineChatWidgetViewState { @@ -299,9 +300,9 @@ export class InlineChatWidget { // LEGACY - default chat model // this is only here for as long as we offer updateChatMessage - this._defaultChatModel = this._store.add(this._instantiationService.createInstance(ChatModel, undefined, ChatAgentLocation.Editor)); - this._defaultChatModel.startInitialize(); - this._defaultChatModel.initialize(undefined); + this._defaultChatModel = this._chatService.startSession(location, CancellationToken.None) ?? this._store.add(this._instantiationService.createInstance(ChatModel, undefined, ChatAgentLocation.Editor)); + // this._defaultChatModel.startInitialize(); + // this._defaultChatModel.initialize(undefined); this.setChatModel(this._defaultChatModel); } From da5e82a46fe436723948ec0198fede38fa14b762 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Tue, 21 May 2024 21:34:35 -0700 Subject: [PATCH 307/357] feat: persist chat attachments in history --- .../browser/actions/chatContextActions.ts | 4 +- .../contrib/chat/browser/chat.contribution.ts | 1 + src/vs/workbench/contrib/chat/browser/chat.ts | 3 +- .../contrib/chat/browser/chatInputPart.ts | 4 ++ .../contrib/chat/browser/chatVariables.ts | 7 +- .../contrib/chat/browser/chatWidget.ts | 10 ++- .../browser/contrib/chatContextAttachments.ts | 65 +++++++++++++++++++ 7 files changed, 88 insertions(+), 6 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/contrib/chatContextAttachments.ts diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index 80a697a7047..92a2c5f23c6 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -19,6 +19,7 @@ import { AnythingQuickAccessProviderRunOptions } from 'vs/platform/quickinput/co import { IQuickInputService, IQuickPickItem, QuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; import { IChatWidget, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; +import { ChatContextAttachments } from 'vs/workbench/contrib/chat/browser/contrib/chatContextAttachments'; import { SelectAndInsertFileAction } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables'; import { ChatAgentLocation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { CONTEXT_CHAT_LOCATION, CONTEXT_IN_CHAT_INPUT } from 'vs/workbench/contrib/chat/common/chatContextKeys'; @@ -76,7 +77,8 @@ class AttachContextAction extends Action2 { toAttach.push({ ...pick, fullName: pick.label }); } } - widget?.attachContext(...toAttach); + + widget.getContrib(ChatContextAttachments.ID)?.setContext(false, ...toAttach); } override async run(accessor: ServicesAccessor, ...args: any[]): Promise { diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 93fd6755645..7d45fc86091 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -41,6 +41,7 @@ import { ChatVariablesService } from 'vs/workbench/contrib/chat/browser/chatVari import { ChatWidgetService } from 'vs/workbench/contrib/chat/browser/chatWidget'; import { ChatCodeBlockContextProviderService } from 'vs/workbench/contrib/chat/browser/codeBlockContextProviderService'; import 'vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib'; +import 'vs/workbench/contrib/chat/browser/contrib/chatContextAttachments'; import 'vs/workbench/contrib/chat/browser/contrib/chatInputCompletions'; import { ChatAgentLocation, ChatAgentNameService, ChatAgentService, IChatAgentNameService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { chatVariableLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index 3a606450f2d..48844b393d8 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -131,6 +131,7 @@ export interface IChatWidget { readonly onDidHide: Event; readonly onDidSubmitAgent: Event<{ agent: IChatAgentData; slashCommand?: IChatAgentCommand }>; readonly onDidChangeParsedInput: Event; + readonly onDidDeleteContext: Event; readonly location: ChatAgentLocation; readonly viewContext: IChatWidgetViewContext; readonly viewModel: IChatViewModel | undefined; @@ -158,7 +159,7 @@ export interface IChatWidget { getCodeBlockInfosForResponse(response: IChatResponseViewModel): IChatCodeBlockInfo[]; getFileTreeInfosForResponse(response: IChatResponseViewModel): IChatFileTreeInfo[]; getLastFocusedFileTreeForResponse(response: IChatResponseViewModel): IChatFileTreeInfo | undefined; - attachContext(...context: IChatRequestVariableEntry[]): void; + setContext(overwrite: boolean, ...context: IChatRequestVariableEntry[]): void; clear(): void; } diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index d857c4cc959..dc56a5c97aa 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -85,6 +85,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private _onDidBlur = this._register(new Emitter()); readonly onDidBlur = this._onDidBlur.event; + private _onDidDeleteContext = this._register(new Emitter()); + readonly onDidDeleteContext = this._onDidDeleteContext.event; + private _onDidAcceptFollowup = this._register(new Emitter<{ followup: IChatFollowup; response: IChatResponseViewModel | undefined }>()); readonly onDidAcceptFollowup = this._onDidAcceptFollowup.event; @@ -456,6 +459,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.attachedContext.delete(attachment); disp.dispose(); this._onDidChangeHeight.fire(); + this._onDidDeleteContext.fire(attachment); }); this.attachedContextDisposables.add(disp); } diff --git a/src/vs/workbench/contrib/chat/browser/chatVariables.ts b/src/vs/workbench/contrib/chat/browser/chatVariables.ts index 5207c6d6a53..4dad704cf2c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatVariables.ts +++ b/src/vs/workbench/contrib/chat/browser/chatVariables.ts @@ -18,6 +18,7 @@ import { IChatModel, IChatRequestVariableData, IChatRequestVariableEntry } from import { ChatRequestDynamicVariablePart, ChatRequestVariablePart, IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatContentReference } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatRequestVariableValue, IChatVariableData, IChatVariableResolver, IChatVariableResolverProgress, IChatVariablesService, IDynamicVariable } from 'vs/workbench/contrib/chat/common/chatVariables'; +import { ChatContextAttachments } from 'vs/workbench/contrib/chat/browser/contrib/chatContextAttachments'; interface IChatData { data: IChatVariableData; @@ -64,7 +65,7 @@ export class ChatVariablesService implements IChatVariablesService { attachedContextVariables ?.forEach((attachment, i) => { - const data = this._resolver.get(attachment.name.toLowerCase()); + const data = this._resolver.get(attachment.name?.toLowerCase()); if (data) { const references: IChatContentReference[] = []; const variableProgressCallback = (item: IChatVariableResolverProgress) => { @@ -161,7 +162,7 @@ export class ChatVariablesService implements IChatVariablesService { if (key === 'file' && typeof value !== 'string') { const uri = URI.isUri(value) ? value : value.uri; const range = 'range' in value ? value.range : undefined; - widget.attachContext({ value, id: uri.toString() + (range?.toString() ?? ''), name: basename(uri.path), isDynamic: true }); + widget.getContrib(ChatContextAttachments.ID)?.setContext(false, { value, id: uri.toString() + (range?.toString() ?? ''), name: basename(uri.path), isDynamic: true }); return; } @@ -170,6 +171,6 @@ export class ChatVariablesService implements IChatVariablesService { return; } - widget.attachContext({ ...resolved.data, value }); + widget.getContrib(ChatContextAttachments.ID)?.setContext(false, { ...resolved.data, value }); } } diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 0024b27a413..6b19f886cf1 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -96,6 +96,9 @@ export class ChatWidget extends Disposable implements IChatWidget { private _onDidAcceptInput = this._register(new Emitter()); readonly onDidAcceptInput = this._onDidAcceptInput.event; + private _onDidDeleteContext = this._register(new Emitter()); + readonly onDidDeleteContext = this._onDidDeleteContext.event; + private _onDidHide = this._register(new Emitter()); readonly onDidHide = this._onDidHide.event; @@ -544,6 +547,7 @@ export class ChatWidget extends Disposable implements IChatWidget { }); })); this._register(this.inputPart.onDidFocus(() => this._onDidFocus.fire())); + this._register(this.inputPart.onDidDeleteContext((e) => this._onDidDeleteContext.fire(e))); this._register(this.inputPart.onDidAcceptFollowup(e => { if (!this.viewModel) { return; @@ -731,8 +735,12 @@ export class ChatWidget extends Disposable implements IChatWidget { } - attachContext(...contentReferences: IChatRequestVariableEntry[]) { + setContext(overwrite: boolean, ...contentReferences: IChatRequestVariableEntry[]) { + if (overwrite) { + this.inputPart.attachedContext.clear(); + } this.inputPart.attachContext(...contentReferences); + if (this.bodyDimension) { this.layout(this.bodyDimension.height, this.bodyDimension.width); } diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatContextAttachments.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatContextAttachments.ts new file mode 100644 index 00000000000..c70b4d7e5a5 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatContextAttachments.ts @@ -0,0 +1,65 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { IChatWidget } from 'vs/workbench/contrib/chat/browser/chat'; +import { ChatWidget, IChatWidgetContrib } from 'vs/workbench/contrib/chat/browser/chatWidget'; +import { IChatRequestVariableEntry } from 'vs/workbench/contrib/chat/common/chatModel'; + +export class ChatContextAttachments extends Disposable implements IChatWidgetContrib { + + private _attachedContext = new Set(); + + public static readonly ID = 'chatContextAttachments'; + + get id() { + return ChatContextAttachments.ID; + } + + constructor(readonly widget: IChatWidget) { + super(); + + this._register(this.widget.onDidDeleteContext((e) => { + this._removeContext(e); + })); + + this._register(this.widget.onDidSubmitAgent(() => { + this._clearAttachedContext(); + })); + } + + getInputState?() { + return [...this._attachedContext.values()]; + } + + setInputState?(s: any): void { + if (!Array.isArray(s)) { + return; + } + + this.widget.setContext(true, ...s); + } + + setContext(overwrite: boolean, ...attachments: IChatRequestVariableEntry[]) { + if (overwrite) { + this._attachedContext.clear(); + } + for (const attachment of attachments) { + this._attachedContext.add(attachment); + } + + this.widget.setContext(overwrite, ...attachments); + } + + private _removeContext(attachment: IChatRequestVariableEntry) { + this._attachedContext.delete(attachment); + } + + private _clearAttachedContext() { + this._attachedContext.clear(); + } +} + +ChatWidget.CONTRIBS.push(ChatContextAttachments); From 4fb755cadb12588a3eefb1ef709cd0e489cb5799 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Tue, 21 May 2024 21:36:07 -0700 Subject: [PATCH 308/357] Fix "open chat in editor" (#213198) Fix microsoft/vscode-copilot-release#1233 --- src/vs/workbench/contrib/chat/common/chatServiceImpl.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index 296677d5bf3..4b093e11637 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -742,7 +742,9 @@ export class ChatService extends Disposable implements IChatService { } if (model.initialLocation === ChatAgentLocation.Panel) { - this._persistedSessions[sessionId] = model.toJSON(); + // Turn all the real objects into actual JSON, otherwise, calling 'revive' may fail when it tries to + // assign values to properties that are getters- microsoft/vscode-copilot-release#1233 + this._persistedSessions[sessionId] = JSON.parse(JSON.stringify(model)); } this._sessionModels.deleteAndDispose(sessionId); From 95ffd765e1336e8cc1bf9ba16d6d4e84f49fd76b Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 22 May 2024 09:07:35 +0200 Subject: [PATCH 309/357] chore - remove dead/stale commands (#213207) --- .../browser/inlineChat.contribution.ts | 2 - .../inlineChat/browser/inlineChatActions.ts | 78 +------------------ .../contrib/inlineChat/common/inlineChat.ts | 1 - 3 files changed, 3 insertions(+), 78 deletions(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts index 2edb4d039c5..397a916905d 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts @@ -37,8 +37,6 @@ registerAction2(InlineChatActions.ConfigureInlineChatAction); registerAction2(InlineChatActions.UnstashSessionAction); registerAction2(InlineChatActions.DiscardHunkAction); registerAction2(InlineChatActions.DiscardAction); -registerAction2(InlineChatActions.DiscardToClipboardAction); -registerAction2(InlineChatActions.DiscardUndoToNewFileAction); registerAction2(InlineChatActions.RerunAction); registerAction2(InlineChatActions.CancelSessionAction); registerAction2(InlineChatActions.MoveToNextHunk); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index fe495060020..472bd45cf46 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -11,16 +11,15 @@ import { EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/em import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { InlineChatController, InlineChatRunOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; -import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_PROVIDER, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_VISIBLE, MENU_INLINE_CHAT_WIDGET_DISCARD, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_EDIT_MODE, EditMode, CTX_INLINE_CHAT_DOCUMENT_CHANGED, CTX_INLINE_CHAT_DID_EDIT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, ACTION_ACCEPT_CHANGES, CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChatResponseTypes, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, MENU_INLINE_CHAT_WIDGET, ACTION_TOGGLE_DIFF, ACTION_REGENERATE_RESPONSE } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_PROVIDER, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_VISIBLE, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_EDIT_MODE, EditMode, CTX_INLINE_CHAT_DOCUMENT_CHANGED, CTX_INLINE_CHAT_HAS_STASHED_SESSION, ACTION_ACCEPT_CHANGES, CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChatResponseTypes, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, MENU_INLINE_CHAT_WIDGET, ACTION_TOGGLE_DIFF, ACTION_REGENERATE_RESPONSE } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { localize, localize2 } from 'vs/nls'; -import { Action2, IAction2Options, MenuRegistry } from 'vs/platform/actions/common/actions'; +import { Action2, IAction2Options } from 'vs/platform/actions/common/actions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; -import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; -import { IUntitledTextResourceEditorInput } from 'vs/workbench/common/editor'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { fromNow } from 'vs/base/common/date'; import { IInlineChatSessionService, Recording } from './inlineChatSessionService'; @@ -250,18 +249,6 @@ export class DiscardHunkAction extends AbstractInlineChatAction { } } - -MenuRegistry.appendMenuItem(MENU_INLINE_CHAT_WIDGET_STATUS, { - submenu: MENU_INLINE_CHAT_WIDGET_DISCARD, - title: localize('discardMenu', "Discard..."), - icon: Codicon.discard, - group: '0_main', - order: 2, - when: ContextKeyExpr.and(CTX_INLINE_CHAT_EDIT_MODE.notEqualsTo(EditMode.Preview), CTX_INLINE_CHAT_EDIT_MODE.notEqualsTo(EditMode.Live), CTX_INLINE_CHAT_RESPONSE_TYPES.notEqualsTo(InlineChatResponseTypes.OnlyMessages)), - rememberDefaultAction: true -}); - - export class DiscardAction extends AbstractInlineChatAction { constructor() { @@ -274,11 +261,6 @@ export class DiscardAction extends AbstractInlineChatAction { weight: KeybindingWeight.EditorContrib - 1, primary: KeyCode.Escape, when: CTX_INLINE_CHAT_USER_DID_EDIT.negate() - }, - menu: { - id: MENU_INLINE_CHAT_WIDGET_DISCARD, - group: '0_main', - order: 0 } }); } @@ -288,60 +270,6 @@ export class DiscardAction extends AbstractInlineChatAction { } } -export class DiscardToClipboardAction extends AbstractInlineChatAction { - - constructor() { - super({ - id: 'inlineChat.discardToClipboard', - title: localize('undo.clipboard', 'Discard to Clipboard'), - precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_DID_EDIT), - // keybinding: { - // weight: KeybindingWeight.EditorContrib + 10, - // primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyZ, - // mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyZ }, - // }, - menu: { - id: MENU_INLINE_CHAT_WIDGET_DISCARD, - group: '0_main', - order: 1 - } - }); - } - - override async runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController): Promise { - const clipboardService = accessor.get(IClipboardService); - const changedText = await ctrl.cancelSession(); - if (changedText !== undefined) { - clipboardService.writeText(changedText); - } - } -} - -export class DiscardUndoToNewFileAction extends AbstractInlineChatAction { - - constructor() { - super({ - id: 'inlineChat.discardToFile', - title: localize('undo.newfile', 'Discard to New File'), - precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_DID_EDIT), - menu: { - id: MENU_INLINE_CHAT_WIDGET_DISCARD, - group: '0_main', - order: 2 - } - }); - } - - override async runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController, editor: ICodeEditor, ..._args: any[]): Promise { - const editorService = accessor.get(IEditorService); - const changedText = await ctrl.cancelSession(); - if (changedText !== undefined) { - const input: IUntitledTextResourceEditorInput = { forceUntitled: true, resource: undefined, contents: changedText, languageId: editor.getModel()?.getLanguageId() }; - editorService.openEditor(input, SIDE_GROUP); - } - } -} - export class ToggleDiffForChange extends AbstractInlineChatAction { constructor() { diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index 59bb7cafe09..c9b7c617a7d 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -192,7 +192,6 @@ export const ACTION_TOGGLE_DIFF = 'inlineChat.toggleDiff'; export const MENU_INLINE_CHAT_WIDGET = MenuId.for('inlineChatWidget'); export const MENU_INLINE_CHAT_WIDGET_STATUS = MenuId.for('inlineChatWidget.status'); -export const MENU_INLINE_CHAT_WIDGET_DISCARD = MenuId.for('inlineChatWidget.undo'); // --- colors From b46315d1434117e24e6bc967140b6fc9fd5e3077 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 22 May 2024 09:29:39 +0200 Subject: [PATCH 310/357] chore - write error message so that it doesn't get confused as file path (#213210) --- src/vs/base/common/event.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/base/common/event.ts b/src/vs/base/common/event.ts index 7cefcdcedbd..4ab515e5253 100644 --- a/src/vs/base/common/event.ts +++ b/src/vs/base/common/event.ts @@ -1066,7 +1066,7 @@ export class Emitter { get event(): Event { this._event ??= (callback: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => { if (this._leakageMon && this._size > this._leakageMon.threshold * 3) { - const message = `[${this._leakageMon.name}] REFUSES to accept new listeners because it exceeded its threshold by far (${this._size}/${this._leakageMon.threshold})`; + const message = `[${this._leakageMon.name}] REFUSES to accept new listeners because it exceeded its threshold by far (${this._size} vs ${this._leakageMon.threshold})`; console.warn(message); const tuple = this._leakageMon.getMostFrequentStack() ?? ['UNKNOWN stack', -1]; From 51739d8d28bc45100b33315d0119ed0ead84f8b1 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 22 May 2024 09:45:20 +0200 Subject: [PATCH 311/357] Perf: visualise memory efficiency (#211837) (#213208) * Perf: visualise memory efficiency (#211837) * feat: add telemetry logging for startup heap statistics * fix compile * feat: add used heap size to startup performance telemetry * refactor: remove allocated heap size from startup performance telemetry * comments --- .../parts/sandbox/common/electronTypes.ts | 27 +++- .../sandbox/electron-sandbox/electronTypes.ts | 26 +--- .../parts/sandbox/electron-sandbox/globals.ts | 5 + .../parts/sandbox/electron-sandbox/preload.js | 1 + .../base/parts/sandbox/node/electronTypes.ts | 3 + src/vs/platform/environment/common/argv.ts | 4 + src/vs/platform/environment/node/argv.ts | 4 + .../electron-sandbox/startupTimings.ts | 126 +++++++++++++++++- .../services/timer/browser/timerService.ts | 15 +++ 9 files changed, 181 insertions(+), 30 deletions(-) diff --git a/src/vs/base/parts/sandbox/common/electronTypes.ts b/src/vs/base/parts/sandbox/common/electronTypes.ts index f8c7a35e077..43fa75079d4 100644 --- a/src/vs/base/parts/sandbox/common/electronTypes.ts +++ b/src/vs/base/parts/sandbox/common/electronTypes.ts @@ -7,7 +7,7 @@ // ####################################################################### // ### ### // ### electron.d.ts types we need in a common layer for reuse ### -// ### (copied from Electron 16.x) ### +// ### (copied from Electron 29.x) ### // ### ### // ####################################################################### @@ -148,9 +148,9 @@ export interface SaveDialogReturnValue { */ canceled: boolean; /** - * If the dialog is canceled, this will be `undefined`. + * If the dialog is canceled, this will be an empty string. */ - filePath?: string; + filePath: string; /** * Base64 encoded string which contains the security scoped bookmark data for the * saved file. `securityScopedBookmarks` must be enabled for this to be present. @@ -219,16 +219,20 @@ export interface FileFilter { export interface OpenDevToolsOptions { /** - * Opens the devtools with specified dock state, can be `right`, `bottom`, + * Opens the devtools with specified dock state, can be `left`, `right`, `bottom`, * `undocked`, `detach`. Defaults to last used dock state. In `undocked` mode it's * possible to dock back. In `detach` mode it's not. */ - mode: ('right' | 'bottom' | 'undocked' | 'detach'); + mode: ('left' | 'right' | 'bottom' | 'undocked' | 'detach'); /** * Whether to bring the opened devtools window to the foreground. The default is * `true`. */ activate?: boolean; + /** + * A title for the DevTools window (only in `undocked` or `detach` mode). + */ + title?: string; } interface InputEvent { @@ -241,6 +245,19 @@ interface InputEvent { * `middleButtonDown`, `rightButtonDown`, `capsLock`, `numLock`, `left`, `right`. */ modifiers?: Array<'shift' | 'control' | 'ctrl' | 'alt' | 'meta' | 'command' | 'cmd' | 'isKeypad' | 'isAutoRepeat' | 'leftButtonDown' | 'middleButtonDown' | 'rightButtonDown' | 'capsLock' | 'numLock' | 'left' | 'right'>; + /** + * Can be `undefined`, `mouseDown`, `mouseUp`, `mouseMove`, `mouseEnter`, + * `mouseLeave`, `contextMenu`, `mouseWheel`, `rawKeyDown`, `keyDown`, `keyUp`, + * `char`, `gestureScrollBegin`, `gestureScrollEnd`, `gestureScrollUpdate`, + * `gestureFlingStart`, `gestureFlingCancel`, `gesturePinchBegin`, + * `gesturePinchEnd`, `gesturePinchUpdate`, `gestureTapDown`, `gestureShowPress`, + * `gestureTap`, `gestureTapCancel`, `gestureShortPress`, `gestureLongPress`, + * `gestureLongTap`, `gestureTwoFingerTap`, `gestureTapUnconfirmed`, + * `gestureDoubleTap`, `touchStart`, `touchMove`, `touchEnd`, `touchCancel`, + * `touchScrollStarted`, `pointerDown`, `pointerUp`, `pointerMove`, + * `pointerRawUpdate`, `pointerCancel` or `pointerCausedUaAction`. + */ + type: ('undefined' | 'mouseDown' | 'mouseUp' | 'mouseMove' | 'mouseEnter' | 'mouseLeave' | 'contextMenu' | 'mouseWheel' | 'rawKeyDown' | 'keyDown' | 'keyUp' | 'char' | 'gestureScrollBegin' | 'gestureScrollEnd' | 'gestureScrollUpdate' | 'gestureFlingStart' | 'gestureFlingCancel' | 'gesturePinchBegin' | 'gesturePinchEnd' | 'gesturePinchUpdate' | 'gestureTapDown' | 'gestureShowPress' | 'gestureTap' | 'gestureTapCancel' | 'gestureShortPress' | 'gestureLongPress' | 'gestureLongTap' | 'gestureTwoFingerTap' | 'gestureTapUnconfirmed' | 'gestureDoubleTap' | 'touchStart' | 'touchMove' | 'touchEnd' | 'touchCancel' | 'touchScrollStarted' | 'pointerDown' | 'pointerUp' | 'pointerMove' | 'pointerRawUpdate' | 'pointerCancel' | 'pointerCausedUaAction'); } export interface MouseInputEvent extends InputEvent { diff --git a/src/vs/base/parts/sandbox/electron-sandbox/electronTypes.ts b/src/vs/base/parts/sandbox/electron-sandbox/electronTypes.ts index d32188d1e97..ba8ea6446a6 100644 --- a/src/vs/base/parts/sandbox/electron-sandbox/electronTypes.ts +++ b/src/vs/base/parts/sandbox/electron-sandbox/electronTypes.ts @@ -7,7 +7,7 @@ // ####################################################################### // ### ### // ### electron.d.ts types we expose from electron-sandbox ### -// ### (copied from Electron 25.x) ### +// ### (copied from Electron 29.x) ### // ### ### // ####################################################################### @@ -30,20 +30,6 @@ export interface IpcRendererEvent extends Event { * The `IpcRenderer` instance that emitted the event originally */ sender: IpcRenderer; - /** - * The `webContents.id` that sent the message, you can call - * `event.sender.sendTo(event.senderId, ...)` to reply to the message, see - * ipcRenderer.sendTo for more information. This only applies to messages sent from - * a different renderer. Messages sent directly from the main process set - * `event.senderId` to `0`. - */ - senderId: number; - /** - * Whether the message sent via ipcRenderer.sendTo was sent by the main frame. This - * is relevant when `nodeIntegrationInSubFrames` is enabled in the originating - * `webContents`. - */ - senderIsMainFrame?: boolean; } export interface IpcRenderer { @@ -91,10 +77,6 @@ export interface IpcRenderer { * only the next time a message is sent to `channel`, after which it is removed. */ once(channel: string, listener: (event: IpcRendererEvent, ...args: any[]) => void): this; - /** - * Removes the specified `listener` from the listener array for the specified - * `channel`. - */ // Note: API with `Transferable` intentionally commented out because you // cannot transfer these when `contextIsolation: true`. // /** @@ -111,7 +93,11 @@ export interface IpcRenderer { // * documentation. // */ // postMessage(channel: string, message: any, transfer?: MessagePort[]): void; - removeListener(channel: string, listener: (...args: any[]) => void): this; + /** + * Removes the specified `listener` from the listener array for the specified + * `channel`. + */ + removeListener(channel: string, listener: (event: IpcRendererEvent, ...args: any[]) => void): this; /** * Send an asynchronous message to the main process via `channel`, along with * arguments. Arguments will be serialized with the Structured Clone Algorithm, diff --git a/src/vs/base/parts/sandbox/electron-sandbox/globals.ts b/src/vs/base/parts/sandbox/electron-sandbox/globals.ts index 44a54904e26..9a01b61c669 100644 --- a/src/vs/base/parts/sandbox/electron-sandbox/globals.ts +++ b/src/vs/base/parts/sandbox/electron-sandbox/globals.ts @@ -12,6 +12,11 @@ import { IpcRenderer, ProcessMemoryInfo, WebFrame } from 'vs/base/parts/sandbox/ */ export interface ISandboxNodeProcess extends INodeProcess { + /** + * The process.pid property returns the process ID of the process. + */ + readonly pid: number; + /** * The process.platform property returns a string identifying the operating system platform * on which the Node.js process is running. diff --git a/src/vs/base/parts/sandbox/electron-sandbox/preload.js b/src/vs/base/parts/sandbox/electron-sandbox/preload.js index 90ac940861f..4c51b45d18b 100644 --- a/src/vs/base/parts/sandbox/electron-sandbox/preload.js +++ b/src/vs/base/parts/sandbox/electron-sandbox/preload.js @@ -248,6 +248,7 @@ * @type {ISandboxNodeProcess} */ process: { + get pid() { return process.pid; }, get platform() { return process.platform; }, get arch() { return process.arch; }, get env() { return { ...process.env }; }, diff --git a/src/vs/base/parts/sandbox/node/electronTypes.ts b/src/vs/base/parts/sandbox/node/electronTypes.ts index 3d108454bc4..37629888c19 100644 --- a/src/vs/base/parts/sandbox/node/electronTypes.ts +++ b/src/vs/base/parts/sandbox/node/electronTypes.ts @@ -11,6 +11,7 @@ export interface MessagePortMain extends NodeJS.EventEmitter { * Emitted when the remote end of a MessagePortMain object becomes disconnected. */ on(event: 'close', listener: Function): this; + off(event: 'close', listener: Function): this; once(event: 'close', listener: Function): this; addListener(event: 'close', listener: Function): this; removeListener(event: 'close', listener: Function): this; @@ -18,6 +19,7 @@ export interface MessagePortMain extends NodeJS.EventEmitter { * Emitted when a MessagePortMain object receives a message. */ on(event: 'message', listener: (messageEvent: MessageEvent) => void): this; + off(event: 'message', listener: (messageEvent: MessageEvent) => void): this; once(event: 'message', listener: (messageEvent: MessageEvent) => void): this; addListener(event: 'message', listener: (messageEvent: MessageEvent) => void): this; removeListener(event: 'message', listener: (messageEvent: MessageEvent) => void): this; @@ -51,6 +53,7 @@ export interface ParentPort extends NodeJS.EventEmitter { * be queued up until a handler is registered for this event. */ on(event: 'message', listener: (messageEvent: MessageEvent) => void): this; + off(event: 'message', listener: (messageEvent: MessageEvent) => void): this; once(event: 'message', listener: (messageEvent: MessageEvent) => void): this; addListener(event: 'message', listener: (messageEvent: MessageEvent) => void): this; removeListener(event: 'message', listener: (messageEvent: MessageEvent) => void): this; diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts index ba2a22ea621..818021ff0c1 100644 --- a/src/vs/platform/environment/common/argv.ts +++ b/src/vs/platform/environment/common/argv.ts @@ -141,4 +141,8 @@ export interface NativeParsedArgs { 'vmodule'?: string; 'disable-dev-shm-usage'?: boolean; 'ozone-platform'?: string; + 'enable-tracing'?: string; + 'trace-startup-format'?: string; + 'trace-startup-file'?: string; + 'trace-startup-duration'?: string; } diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 3b6f55bd851..26b7d9c6937 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -205,6 +205,10 @@ export const OPTIONS: OptionDescriptions> = { 'disable-dev-shm-usage': { type: 'boolean' }, 'profile-temp': { type: 'boolean' }, 'ozone-platform': { type: 'string' }, + 'enable-tracing': { type: 'string' }, + 'trace-startup-format': { type: 'string' }, + 'trace-startup-file': { type: 'string' }, + 'trace-startup-duration': { type: 'string' }, _: { type: 'string[]' } // main arguments }; diff --git a/src/vs/workbench/contrib/performance/electron-sandbox/startupTimings.ts b/src/vs/workbench/contrib/performance/electron-sandbox/startupTimings.ts index 952b68dc613..276c5278bfe 100644 --- a/src/vs/workbench/contrib/performance/electron-sandbox/startupTimings.ts +++ b/src/vs/workbench/contrib/performance/electron-sandbox/startupTimings.ts @@ -20,6 +20,26 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; import { StartupTimings } from 'vs/workbench/contrib/performance/browser/startupTimings'; +import { process } from 'vs/base/parts/sandbox/electron-sandbox/globals'; +import { coalesce } from 'vs/base/common/arrays'; + +interface ITracingData { + readonly args?: { + readonly usedHeapSizeAfter?: number; + readonly usedHeapSizeBefore?: number; + }; + readonly dur: number; // in microseconds + readonly name: string; // e.g. MinorGC or MajorGC + readonly pid: number; +} + +interface IHeapStatistics { + readonly used: number; + readonly garbage: number; + readonly majorGCs: number; + readonly minorGCs: number; + readonly duration: number; +} export class NativeStartupTimings extends StartupTimings implements IWorkbenchContribution { @@ -34,7 +54,7 @@ export class NativeStartupTimings extends StartupTimings implements IWorkbenchCo @IUpdateService updateService: IUpdateService, @INativeWorkbenchEnvironmentService private readonly _environmentService: INativeWorkbenchEnvironmentService, @IProductService private readonly _productService: IProductService, - @IWorkspaceTrustManagementService workspaceTrustService: IWorkspaceTrustManagementService, + @IWorkspaceTrustManagementService workspaceTrustService: IWorkspaceTrustManagementService ) { super(editorService, paneCompositeService, lifecycleService, updateService, workspaceTrustService); @@ -62,10 +82,22 @@ export class NativeStartupTimings extends StartupTimings implements IWorkbenchCo ]); const perfBaseline = await this._timerService.perfBaseline; + const heapStatistics = await this._resolveStartupHeapStatistics(); + if (heapStatistics) { + this._telemetryLogHeapStatistics(heapStatistics); + } if (appendTo) { - const content = `${this._timerService.startupMetrics.ellapsed}\t${this._productService.nameShort}\t${(this._productService.commit || '').slice(0, 10) || '0000000000'}\t${this._telemetryService.sessionId}\t${standardStartupError === undefined ? 'standard_start' : 'NO_standard_start : ' + standardStartupError}\t${String(perfBaseline).padStart(4, '0')}ms\n`; - await this.appendContent(URI.file(appendTo), content); + const content = coalesce([ + this._timerService.startupMetrics.ellapsed, + this._productService.nameShort, + (this._productService.commit || '').slice(0, 10) || '0000000000', + this._telemetryService.sessionId, + standardStartupError === undefined ? 'standard_start' : `NO_standard_start : ${standardStartupError}`, + `${String(perfBaseline).padStart(4, '0')}ms`, + heapStatistics ? this._printStartupHeapStatistics(heapStatistics) : undefined + ]).join('\t') + '\n'; + await this._appendContent(URI.file(appendTo), content); } if (durationMarkers?.length) { @@ -88,7 +120,7 @@ export class NativeStartupTimings extends StartupTimings implements IWorkbenchCo const durationsContent = `${durations.join('\t')}\n`; if (durationMarkersFile) { - await this.appendContent(URI.file(durationMarkersFile), durationsContent); + await this._appendContent(URI.file(durationMarkersFile), durationsContent); } else { console.log(durationsContent); } @@ -109,7 +141,7 @@ export class NativeStartupTimings extends StartupTimings implements IWorkbenchCo return super._isStandardStartup(); } - private async appendContent(file: URI, content: string): Promise { + private async _appendContent(file: URI, content: string): Promise { const chunks: VSBuffer[] = []; if (await this._fileService.exists(file)) { chunks.push((await this._fileService.readFile(file)).value); @@ -117,4 +149,88 @@ export class NativeStartupTimings extends StartupTimings implements IWorkbenchCo chunks.push(VSBuffer.fromString(content)); await this._fileService.writeFile(file, VSBuffer.concat(chunks)); } + + private async _resolveStartupHeapStatistics(): Promise { + if ( + !this._environmentService.args['enable-tracing'] || + !this._environmentService.args['trace-startup-file'] || + this._environmentService.args['trace-startup-format'] !== 'json' || + !this._environmentService.args['trace-startup-duration'] + ) { + return undefined; // unexpected arguments for startup heap statistics + } + + const used = (performance as unknown as { memory?: { usedJSHeapSize?: number } }).memory?.usedJSHeapSize ?? 0; // https://developer.mozilla.org/en-US/docs/Web/API/Performance/memory + + let minorGCs = 0; + let majorGCs = 0; + let garbage = 0; + let duration = 0; + + try { + const traceContents: { traceEvents: ITracingData[] } = JSON.parse((await this._fileService.readFile(URI.file(this._environmentService.args['trace-startup-file']))).value.toString()); + for (const event of traceContents.traceEvents) { + if (event.pid !== process.pid) { + continue; + } + + switch (event.name) { + + // Major/Minor GC Events + case 'MinorGC': + minorGCs++; + case 'MajorGC': + majorGCs++; + if (event.args && typeof event.args.usedHeapSizeAfter === 'number' && typeof event.args.usedHeapSizeBefore === 'number') { + garbage += (event.args.usedHeapSizeBefore - event.args.usedHeapSizeAfter); + } + break; + + // GC Events that block the main thread + // Refs: https://v8.dev/blog/trash-talk + case 'V8.GCFinalizeMC': + case 'V8.GCScavenger': + duration += event.dur; + break; + } + } + + return { minorGCs, majorGCs, used, garbage, duration: Math.round(duration / 1000) }; + } catch (error) { + console.error(error); + } + + return undefined; + } + + private _telemetryLogHeapStatistics({ used, garbage, majorGCs, minorGCs, duration }: IHeapStatistics): void { + type StartupHeapStatisticsClassification = { + owner: 'bpasero'; + comment: 'An event that reports startup heap statistics for performance analysis.'; + heapUsed: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Used heap' }; + heapGarbage: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Garbage heap' }; + majorGCs: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Major GCs count' }; + minorGCs: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Minor GCs count' }; + gcsDuration: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'GCs duration' }; + }; + type StartupHeapStatisticsEvent = { + heapUsed: number; + heapGarbage: number; + majorGCs: number; + minorGCs: number; + gcsDuration: number; + }; + this._telemetryService.publicLog2('startupHeapStatistics', { + heapUsed: used, + heapGarbage: garbage, + majorGCs, + minorGCs, + gcsDuration: duration + }); + } + + private _printStartupHeapStatistics({ used, garbage, majorGCs, minorGCs, duration }: IHeapStatistics) { + const MB = 1024 * 1024; + return `Heap: ${Math.round(used / MB)}MB (used) ${Math.round(garbage / MB)}MB (garbage) ${majorGCs} (MajorGC) ${minorGCs} (MinorGC) ${duration}ms (GC duration)`; + } } diff --git a/src/vs/workbench/services/timer/browser/timerService.ts b/src/vs/workbench/services/timer/browser/timerService.ts index 1c752ee7501..df14ee528cc 100644 --- a/src/vs/workbench/services/timer/browser/timerService.ts +++ b/src/vs/workbench/services/timer/browser/timerService.ts @@ -446,6 +446,12 @@ export interface ITimerService { * @param to to mark name */ getDuration(from: string, to: string): number; + + /** + * Return the timestamp of a mark. + * @param mark mark name + */ + getStartTime(mark: string): number; } export const ITimerService = createDecorator('timerService'); @@ -471,6 +477,11 @@ class PerfMarks { return toEntry.startTime - fromEntry.startTime; } + getStartTime(mark: string): number { + const entry = this._findEntry(mark); + return entry ? entry.startTime : -1; + } + private _findEntry(name: string): perf.PerformanceMark | void { for (const [, marks] of this._entries) { for (let i = marks.length - 1; i >= 0; i--) { @@ -601,6 +612,10 @@ export abstract class AbstractTimerService implements ITimerService { return this._marks.getDuration(from, to); } + getStartTime(mark: string): number { + return this._marks.getStartTime(mark); + } + private _reportStartupTimes(metrics: IStartupMetrics): void { // report IStartupMetrics as telemetry /* __GDPR__ From 9512a2e9a1d8cf709f0eb314e9046d0d578dceae Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 22 May 2024 13:36:38 +0200 Subject: [PATCH 312/357] debt - some multi-tabs select cleanup (#213161) --- .../parts/editor/multiEditorTabsControl.ts | 61 +++--- .../parts/editor/multiRowEditorTabsControl.ts | 2 +- .../common/editor/editorGroupModel.ts | 182 ++++++++++-------- .../common/editor/filteredEditorGroupModel.ts | 2 +- 4 files changed, 137 insertions(+), 110 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts index a54b4a431d3..395c83aa7a1 100644 --- a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts @@ -889,14 +889,15 @@ export class MultiEditorTabsControl extends EditorTabsControl { const editor = this.tabsModel.getEditorByIndex(tabIndex); if (editor) { if (e.shiftKey) { - let anchor; + let anchor: EditorInput; if (this.lastSingleSelectSelectedEditor && this.tabsModel.isSelected(this.lastSingleSelectSelectedEditor)) { // The last selected editor is the anchor anchor = this.lastSingleSelectSelectedEditor; } else { // The active editor is the anchor - this.lastSingleSelectSelectedEditor = this.groupView.activeEditor!; - anchor = this.groupView.activeEditor!; + const activeEditor = assertIsDefined(this.groupView.activeEditor); + this.lastSingleSelectSelectedEditor = activeEditor; + anchor = activeEditor; } await this.selectEditorsBetween(editor, anchor); } else if ((e.ctrlKey && !isMacintosh) || (e.metaKey && isMacintosh)) { @@ -1292,7 +1293,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { throw new BugIndicatingError(); } - const selection = this.groupView.selectedEditors; + let selection = this.groupView.selectedEditors; // Unselect editors on other side of anchor in relation to the target let currentIndex = anchorIndex; @@ -1308,7 +1309,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { break; } - selection.filter(editor => !editor.matches(currentEditor)); + selection = selection.filter(editor => !editor.matches(currentEditor)); } // Select editors between anchor and target @@ -1334,7 +1335,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { return; } - let newActiveEditor = this.groupView.activeEditor!; + let newActiveEditor = assertIsDefined(this.groupView.activeEditor); // If active editor is bing unselected then find the most recently opened selected editor // that is not the editor being unselected @@ -1355,7 +1356,8 @@ export class MultiEditorTabsControl extends EditorTabsControl { private async unselectAllEditors(): Promise { if (this.groupView.selectedEditors.length > 1) { - await this.groupView.setSelection(this.groupView.activeEditor!, []); + const activeEditor = assertIsDefined(this.groupView.activeEditor); + await this.groupView.setSelection(activeEditor, []); } } @@ -1643,7 +1645,6 @@ export class MultiEditorTabsControl extends EditorTabsControl { } private doRedrawTabActive(isGroupActive: boolean, allowBorderTop: boolean, editor: EditorInput, tabContainer: HTMLElement, tabActionBar: ActionBar): void { - const isActive = this.tabsModel.isActive(editor); const isSelected = this.tabsModel.isSelected(editor); @@ -1657,7 +1658,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { if (isActive) { const activeTabBorderColorBottom = this.getColor(isGroupActive ? TAB_ACTIVE_BORDER : TAB_UNFOCUSED_ACTIVE_BORDER); tabContainer.classList.toggle('tab-border-bottom', !!activeTabBorderColorBottom); - tabContainer.style.setProperty('--tab-border-bottom-color', activeTabBorderColorBottom?.toString() ?? ''); + tabContainer.style.setProperty('--tab-border-bottom-color', activeTabBorderColorBottom ?? ''); } // Set border TOP if theme defined color @@ -2219,30 +2220,30 @@ export class MultiEditorTabsControl extends EditorTabsControl { else if (this.editorTransfer.hasData(DraggedEditorIdentifier.prototype)) { const data = this.editorTransfer.getData(DraggedEditorIdentifier.prototype); if (Array.isArray(data) && data.length > 0) { - const sourceGroup = data.length ? this.editorPartsView.getGroup(data[0].identifier.groupId) : undefined; - const isLocalMove = sourceGroup === this.groupView; + const sourceGroup = this.editorPartsView.getGroup(data[0].identifier.groupId); + if (sourceGroup) { + for (const de of data) { + const editor = de.identifier.editor; - // Keep the same order when moving / copying editors within the same group - for (const de of data) { - const editor = de.identifier.editor; + // Only allow moving/copying from a single group source + if (sourceGroup.id !== de.identifier.groupId) { + continue; + } - // Only allow moving/copying from a single group source - if (!sourceGroup || sourceGroup.id !== de.identifier.groupId) { - continue; + // Keep the same order when moving / copying editors within the same group + const sourceEditorIndex = sourceGroup.getIndexOfEditor(editor); + if (sourceGroup === this.groupView && sourceEditorIndex < targetEditorIndex) { + targetEditorIndex--; + } + + if (this.isMoveOperation(e, de.identifier.groupId, editor)) { + sourceGroup.moveEditor(editor, this.groupView, { ...options, index: targetEditorIndex }); + } else { + sourceGroup.copyEditor(editor, this.groupView, { ...options, index: targetEditorIndex }); + } + + targetEditorIndex++; } - - const sourceEditorIndex = sourceGroup.getIndexOfEditor(editor); - if (isLocalMove && sourceEditorIndex < targetEditorIndex) { - targetEditorIndex--; - } - - if (this.isMoveOperation(e, de.identifier.groupId, editor)) { - sourceGroup.moveEditor(editor, this.groupView, { ...options, index: targetEditorIndex }); - } else { - sourceGroup.copyEditor(editor, this.groupView, { ...options, index: targetEditorIndex }); - } - - targetEditorIndex++; } } diff --git a/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts index 908b7d85cfb..83823d3ec8f 100644 --- a/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts @@ -199,7 +199,7 @@ export class MultiRowEditorControl extends Disposable implements IEditorTabsCont return this.stickyEditorTabsControl.getHeight() + this.unstickyEditorTabsControl.getHeight(); } - public override dispose(): void { + override dispose(): void { this.parent.classList.toggle('two-tab-bars', false); super.dispose(); diff --git a/src/vs/workbench/common/editor/editorGroupModel.ts b/src/vs/workbench/common/editor/editorGroupModel.ts index d8b7d956adc..6f04a87f062 100644 --- a/src/vs/workbench/common/editor/editorGroupModel.ts +++ b/src/vs/workbench/common/editor/editorGroupModel.ts @@ -183,7 +183,7 @@ export interface IReadonlyEditorGroupModel { isActive(editor: EditorInput | IUntypedEditorInput): boolean; isPinned(editorOrIndex: EditorInput | number): boolean; isSticky(editorOrIndex: EditorInput | number): boolean; - isSelected(editor: EditorInput | number): boolean; + isSelected(editorOrIndex: EditorInput | number): boolean; isTransient(editorOrIndex: EditorInput | number): boolean; isFirst(editor: EditorInput, editors?: EditorInput[]): boolean; isLast(editor: EditorInput, editors?: EditorInput[]): boolean; @@ -222,9 +222,6 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { private selection: EditorInput[] = []; // editors in selected state, first one is active - private set active(editor: EditorInput | null) { - this.selection = editor ? [editor] : []; - } private get active(): EditorInput | null { return this.selection[0] ?? null; } @@ -299,8 +296,8 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { return this.active; } - isActive(editor: EditorInput | IUntypedEditorInput): boolean { - return this.matches(this.active, editor); + isActive(candidate: EditorInput | IUntypedEditorInput): boolean { + return this.matches(this.active, candidate); } get previewEditor(): EditorInput | null { @@ -311,7 +308,7 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { const makeSticky = options?.sticky || (typeof options?.index === 'number' && this.isSticky(options.index)); const makePinned = options?.pinned || options?.sticky; const makeTransient = !!options?.transient; - const makeActive = options?.active || !this.activeEditor || (!makePinned && this.matches(this.preview, this.activeEditor)); + const makeActive = options?.active || !this.activeEditor || (!makePinned && this.preview === this.activeEditor); const existingEditorAndIndex = this.findEditor(candidate, options); @@ -413,7 +410,7 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { }; this._onDidModelChange.fire(event); - // Handle active & selection + // Handle active editor / selected editors this.setSelection(makeActive ? newEditor : this.activeEditor, options?.inactiveSelection ?? []); return { @@ -434,7 +431,7 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { this.doPin(existingEditor, existingEditorIndex); } - // Activate / select it + // Handle active editor / selected editors this.setSelection(makeActive ? existingEditor : this.activeEditor, options?.inactiveSelection ?? []); // Respect index @@ -553,8 +550,8 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { const editor = this.editors[index]; const sticky = this.isSticky(index); - // Active Editor closed - const isActiveEditor = this.matches(this.active, editor); + // Active editor closed + const isActiveEditor = this.active === editor; if (openNext && isActiveEditor) { // More than one editor @@ -570,27 +567,29 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { } } - const newSelection = this.selection.filter(selected => !selected.matches(newActive) && !selected.matches(editor)); - this.doSetSelection(newActive, this.editors.indexOf(newActive), newSelection); + // Select editor as active + const newInactiveSelectedEditors = this.selection.filter(selected => selected !== editor && selected !== newActive); + this.doSetSelection(newActive, this.editors.indexOf(newActive), newInactiveSelectedEditors); } - // One Editor + // Last editor closed: clear selection else { - this.active = null; + this.doSetSelection(null, undefined, []); } } - // Remove from selection + // Inactive editor closed else if (!isActiveEditor) { - const wasSelected = !!this.selection.find(selected => this.matches(selected, editor)); - if (wasSelected) { - const newSelection = this.selection.filter(selected => !selected.matches(editor)); - this.doSetSelection(this.activeEditor!, this.indexOf(this.activeEditor), newSelection); + + // Remove editor from inactive selection + if (this.doIsSelected(editor)) { + const newInactiveSelectedEditors = this.selection.filter(selected => selected !== editor && selected !== this.activeEditor); + this.doSetSelection(this.activeEditor, this.indexOf(this.activeEditor), newInactiveSelectedEditors); } } // Preview Editor closed - if (this.matches(this.preview, editor)) { + if (this.preview === editor) { this.preview = null; } @@ -685,72 +684,99 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { const [editor, editorIndex] = res; - this.doSetActive(editor, editorIndex); + this.doSetSelection(editor, editorIndex, []); return editor; } - private doSetActive(editor: EditorInput, editorIndex: number): void { - if (this.matches(this.active, editor)) { - this.selection = [editor]; - return; // already active + get selectedEditors(): EditorInput[] { + return this.editors.filter(editor => this.doIsSelected(editor)); // return in sequential order + } + + isSelected(editorCandidateOrIndex: EditorInput | number): boolean { + let editor: EditorInput | undefined; + if (typeof editorCandidateOrIndex === 'number') { + editor = this.editors[editorCandidateOrIndex]; + } else { + editor = this.findEditor(editorCandidateOrIndex)?.[0]; } - this.active = editor; - - // Bring to front in MRU list - const mruIndex = this.indexOf(editor, this.mru); - this.mru.splice(mruIndex, 1); - this.mru.unshift(editor); - - // Event - const event: IGroupEditorChangeEvent = { - kind: GroupModelChangeKind.EDITOR_ACTIVE, - editor, - editorIndex - }; - this._onDidModelChange.fire(event); + return !!editor && this.doIsSelected(editor); } - public get selectedEditors(): EditorInput[] { - // Return selected editors in sequential order - return this.editors.filter(editor => this.isSelected(editor)); + private doIsSelected(editor: EditorInput): boolean { + return this.selection.includes(editor); } - isSelected(editor: EditorInput | number): boolean { - if (typeof editor === 'number') { - editor = this.editors[editor]; - } - - return !!this.selection.find(selectedEditor => this.matches(selectedEditor, editor)); - } - - setSelection(activeSelectedEditor: EditorInput, inactiveSelectedEditors: EditorInput[]): void { - const res = this.findEditor(activeSelectedEditor); + setSelection(activeSelectedEditorCandidate: EditorInput, inactiveSelectedEditorCandidates: EditorInput[]): void { + const res = this.findEditor(activeSelectedEditorCandidate); if (!res) { - return; + return; // not found } - const [newActiveEditor, newActiveEditorIndex] = res; + const [activeSelectedEditor, activeSelectedEditorIndex] = res; - this.doSetSelection(newActiveEditor, newActiveEditorIndex, inactiveSelectedEditors); + const inactiveSelectedEditors = new Set(); + for (const inactiveSelectedEditorCandidate of inactiveSelectedEditorCandidates) { + const res = this.findEditor(inactiveSelectedEditorCandidate); + if (!res) { + return; // not found + } + + const [inactiveSelectedEditor] = res; + if (inactiveSelectedEditor === activeSelectedEditor) { + continue; // already selected + } + + inactiveSelectedEditors.add(inactiveSelectedEditor); + } + + this.doSetSelection(activeSelectedEditor, activeSelectedEditorIndex, Array.from(inactiveSelectedEditors)); } - private doSetSelection(newActiveEditor: EditorInput, activeEditorIndex: number, inactiveSelectedEditors: EditorInput[]): void { - this.doSetActive(newActiveEditor, activeEditorIndex); + private doSetSelection(activeSelectedEditor: EditorInput | null, activeSelectedEditorIndex: number | undefined, inactiveSelectedEditors: EditorInput[]): void { + const previousActiveEditor = this.activeEditor; + const previousSelection = this.selection; - for (const candidate of inactiveSelectedEditors) { - const editor = this.findEditor(candidate)?.[0]; - if (editor && !this.isSelected(editor)) { - this.selection.push(editor); - } + let newSelection: EditorInput[]; + if (activeSelectedEditor) { + newSelection = [activeSelectedEditor, ...inactiveSelectedEditors]; + } else { + newSelection = []; } - // Event - const event: IGroupModelChangeEvent = { - kind: GroupModelChangeKind.EDITORS_SELECTION, - }; - this._onDidModelChange.fire(event); + // Update selection + this.selection = newSelection; + + // Update active editor if it has changed + const activeEditorChanged = activeSelectedEditor && typeof activeSelectedEditorIndex === 'number' && previousActiveEditor !== activeSelectedEditor; + if (activeEditorChanged) { + + // Bring to front in MRU list + const mruIndex = this.indexOf(activeSelectedEditor, this.mru); + this.mru.splice(mruIndex, 1); + this.mru.unshift(activeSelectedEditor); + + // Event + const event: IGroupEditorChangeEvent = { + kind: GroupModelChangeKind.EDITOR_ACTIVE, + editor: activeSelectedEditor, + editorIndex: activeSelectedEditorIndex + }; + this._onDidModelChange.fire(event); + } + + // Fire event if the selection has changed + if ( + activeEditorChanged || + previousSelection.length !== newSelection.length || + previousSelection.some(editor => !newSelection.includes(editor)) + ) { + const event: IGroupModelChangeEvent = { + kind: GroupModelChangeKind.EDITORS_SELECTION + }; + this._onDidModelChange.fire(event); + } } setIndex(index: number) { @@ -838,12 +864,12 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { } } - isPinned(editorOrIndex: EditorInput | number): boolean { + isPinned(editorCandidateOrIndex: EditorInput | number): boolean { let editor: EditorInput; - if (typeof editorOrIndex === 'number') { - editor = this.editors[editorOrIndex]; + if (typeof editorCandidateOrIndex === 'number') { + editor = this.editors[editorCandidateOrIndex]; } else { - editor = editorOrIndex; + editor = editorCandidateOrIndex; } return !this.matches(this.preview, editor); @@ -980,16 +1006,16 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { this._onDidModelChange.fire(event); } - isTransient(editorOrIndex: EditorInput | number): boolean { + isTransient(editorCandidateOrIndex: EditorInput | number): boolean { if (this.transient.size === 0) { return false; // no transient editor } let editor: EditorInput | undefined; - if (typeof editorOrIndex === 'number') { - editor = this.editors[editorOrIndex]; + if (typeof editorCandidateOrIndex === 'number') { + editor = this.editors[editorCandidateOrIndex]; } else { - editor = this.findEditor(editorOrIndex)?.[0]; + editor = this.findEditor(editorCandidateOrIndex)?.[0]; } return !!editor && this.transient.has(editor); @@ -1140,7 +1166,7 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { clone.editors = this.editors.slice(0); clone.mru = this.mru.slice(0); clone.preview = this.preview; - clone.active = this.active; + clone.selection = this.selection.slice(0); clone.sticky = this.sticky; // Ensure to register listeners for each editor @@ -1242,7 +1268,7 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { this.mru = coalesce(data.mru.map(i => this.editors[i])); - this.active = this.mru[0]; + this.selection = this.mru.length > 0 ? [this.mru[0]] : []; if (typeof data.preview === 'number') { this.preview = this.editors[data.preview]; diff --git a/src/vs/workbench/common/editor/filteredEditorGroupModel.ts b/src/vs/workbench/common/editor/filteredEditorGroupModel.ts index fcda0bbed00..61d4f6a7c80 100644 --- a/src/vs/workbench/common/editor/filteredEditorGroupModel.ts +++ b/src/vs/workbench/common/editor/filteredEditorGroupModel.ts @@ -42,7 +42,7 @@ abstract class FilteredEditorGroupModel extends Disposable implements IReadonlyE isTransient(editorOrIndex: EditorInput | number): boolean { return this.model.isTransient(editorOrIndex); } isSticky(editorOrIndex: EditorInput | number): boolean { return this.model.isSticky(editorOrIndex); } isActive(editor: EditorInput | IUntypedEditorInput): boolean { return this.model.isActive(editor); } - isSelected(editor: EditorInput | number): boolean { return this.model.isSelected(editor); } + isSelected(editorOrIndex: EditorInput | number): boolean { return this.model.isSelected(editorOrIndex); } isFirst(editor: EditorInput): boolean { return this.model.isFirst(editor, this.getEditors(EditorsOrder.SEQUENTIAL)); From 73036af1c924af3567f549bc8536a26a7f63db1a Mon Sep 17 00:00:00 2001 From: Robo Date: Wed, 22 May 2024 22:57:53 +0900 Subject: [PATCH 313/357] chore: update deps for linux x64 client (#213221) chore: update linux client dependencies --- build/linux/debian/dep-lists.js | 3 +-- build/linux/debian/dep-lists.ts | 3 +-- build/linux/rpm/dep-lists.js | 2 -- build/linux/rpm/dep-lists.ts | 2 -- 4 files changed, 2 insertions(+), 8 deletions(-) diff --git a/build/linux/debian/dep-lists.js b/build/linux/debian/dep-lists.js index bdb265b6fec..d843c090063 100644 --- a/build/linux/debian/dep-lists.js +++ b/build/linux/debian/dep-lists.js @@ -57,8 +57,7 @@ exports.referenceGeneratedDepsByArch = { 'libxkbcommon0 (>= 0.5.0)', 'libxkbfile1 (>= 1:1.1.0)', 'libxrandr2', - 'xdg-utils (>= 1.0.2)', - 'zlib1g (>= 1:1.2.3.4)' + 'xdg-utils (>= 1.0.2)' ], 'armhf': [ 'ca-certificates', diff --git a/build/linux/debian/dep-lists.ts b/build/linux/debian/dep-lists.ts index 3d6c2eba6e9..4028370cd02 100644 --- a/build/linux/debian/dep-lists.ts +++ b/build/linux/debian/dep-lists.ts @@ -57,8 +57,7 @@ export const referenceGeneratedDepsByArch = { 'libxkbcommon0 (>= 0.5.0)', 'libxkbfile1 (>= 1:1.1.0)', 'libxrandr2', - 'xdg-utils (>= 1.0.2)', - 'zlib1g (>= 1:1.2.3.4)' + 'xdg-utils (>= 1.0.2)' ], 'armhf': [ 'ca-certificates', diff --git a/build/linux/rpm/dep-lists.js b/build/linux/rpm/dep-lists.js index 5bdcac609c8..8be477290bb 100644 --- a/build/linux/rpm/dep-lists.js +++ b/build/linux/rpm/dep-lists.js @@ -111,8 +111,6 @@ exports.referenceGeneratedDepsByArch = { 'libxkbcommon.so.0()(64bit)', 'libxkbcommon.so.0(V_0.5.0)(64bit)', 'libxkbfile.so.1()(64bit)', - 'libz.so.1()(64bit)', - 'libz.so.1(ZLIB_1.2.3.4)(64bit)', 'rpmlib(FileDigests) <= 4.6.0-1', 'rtld(GNU_HASH)', 'xdg-utils' diff --git a/build/linux/rpm/dep-lists.ts b/build/linux/rpm/dep-lists.ts index 3eb2239aa00..24b18d504c8 100644 --- a/build/linux/rpm/dep-lists.ts +++ b/build/linux/rpm/dep-lists.ts @@ -110,8 +110,6 @@ export const referenceGeneratedDepsByArch = { 'libxkbcommon.so.0()(64bit)', 'libxkbcommon.so.0(V_0.5.0)(64bit)', 'libxkbfile.so.1()(64bit)', - 'libz.so.1()(64bit)', - 'libz.so.1(ZLIB_1.2.3.4)(64bit)', 'rpmlib(FileDigests) <= 4.6.0-1', 'rtld(GNU_HASH)', 'xdg-utils' From d75d42f9bbde5d70565f9b0ce50b7ce44708b258 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 22 May 2024 16:18:02 +0200 Subject: [PATCH 314/357] editors - fix context on `EDITOR_CLOSE` (#213223) --- src/vs/workbench/browser/parts/editor/editorGroupView.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 14beefacad6..e4408c37b17 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -334,6 +334,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView { groupActiveEditorStickyContext.set(this.model.activeEditor ? this.model.isSticky(this.model.activeEditor) : false); break; case GroupModelChangeKind.EDITOR_CLOSE: + groupActiveEditorPinnedContext.set(this.model.activeEditor ? this.model.isPinned(this.model.activeEditor) : false); + groupActiveEditorStickyContext.set(this.model.activeEditor ? this.model.isSticky(this.model.activeEditor) : false); case GroupModelChangeKind.EDITOR_OPEN: case GroupModelChangeKind.EDITOR_MOVE: groupActiveEditorFirstContext.set(this.model.isFirst(this.model.activeEditor)); From dc11635b7339ad5efcdd7f0dee425506593771ec Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 22 May 2024 14:20:38 +0000 Subject: [PATCH 315/357] SCM - add command to focus input (#213225) --- .../contrib/scm/browser/scmViewPane.ts | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index b7df9b4c0e7..185aa10316f 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -38,7 +38,7 @@ import { FileKind } from 'vs/platform/files/common/files'; import { compareFileNames, comparePaths } from 'vs/base/common/comparers'; import { FuzzyScore, createMatches, IMatch } from 'vs/base/common/filters'; import { IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views'; -import { localize } from 'vs/nls'; +import { localize, localize2 } from 'vs/nls'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; import { SIDE_BAR_BACKGROUND, PANEL_BACKGROUND } from 'vs/workbench/common/theme'; @@ -109,6 +109,7 @@ import { IHoverService } from 'vs/platform/hover/browser/hover'; import { OpenScmGroupAction } from 'vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver'; import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; import { ITextModel } from 'vs/editor/common/model'; +import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; // type SCMResourceTreeNode = IResourceNode; // type SCMHistoryItemChangeResourceTreeNode = IResourceNode; @@ -1957,6 +1958,26 @@ class ExpandAllRepositoriesAction extends ViewAction { registerAction2(CollapseAllRepositoriesAction); registerAction2(ExpandAllRepositoriesAction); +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.scm.action.focusInput', + title: { ...localize2('focusInput', "Focus Input") }, + category: localize2('source control', "Source Control"), + precondition: ContextKeys.RepositoryCount.notEqualsTo(0), + f1: true + }); + } + + override async run(accessor: ServicesAccessor) { + const viewsService = accessor.get(IViewsService); + const scmView = await viewsService.openView(VIEW_PANE_ID); + if (scmView) { + scmView.focusInput(); + } + } +}); + const enum SCMInputWidgetCommandId { CancelAction = 'scm.input.cancelAction' } @@ -3432,6 +3453,19 @@ export class SCMViewPane extends ViewPane { } } + focusInput(): void { + this.treeOperationSequencer.queue(() => { + return new Promise(resolve => { + if (this.scmViewService.focusedRepository) { + this.tree.reveal(this.scmViewService.focusedRepository.input, 0.5); + this.inputRenderer.getRenderedInputWidget(this.scmViewService.focusedRepository.input)?.focus(); + } + + resolve(); + }); + }); + } + override shouldShowWelcome(): boolean { return this.scmService.repositoryCount === 0; } From e68a0836fe819f3738e94bf5f2e2fc34300fb7f9 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 22 May 2024 07:58:35 -0700 Subject: [PATCH 316/357] allow customizing accessibility signal delays (#212834) --- .../standalone/browser/standaloneServices.ts | 4 ++ .../browser/accessibilitySignalService.ts | 13 +++- .../browser/accessibilityConfiguration.ts | 70 ++++++++++++++++++- .../editorTextPropertySignalsContribution.ts | 21 +----- 4 files changed, 87 insertions(+), 21 deletions(-) diff --git a/src/vs/editor/standalone/browser/standaloneServices.ts b/src/vs/editor/standalone/browser/standaloneServices.ts index 31ee2f5038b..310436b568f 100644 --- a/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/src/vs/editor/standalone/browser/standaloneServices.ts @@ -1083,6 +1083,10 @@ class StandaloneAccessbilitySignalService implements IAccessibilitySignalService return ValueWithChangeEvent.const(false); } + getDelayMs(signal: AccessibilitySignal, modality: AccessibilityModality): number { + return 0; + } + isSoundEnabled(cue: AccessibilitySignal): boolean { return false; } diff --git a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts index 36d1d1c3f9b..65515c237ad 100644 --- a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts +++ b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts @@ -26,7 +26,7 @@ export interface IAccessibilitySignalService { playSignalLoop(signal: AccessibilitySignal, milliseconds: number): IDisposable; getEnabledState(signal: AccessibilitySignal, userGesture: boolean, modality?: AccessibilityModality | undefined): IValueWithChangeEvent; - + getDelayMs(signal: AccessibilitySignal, modality: AccessibilityModality): number; /** * Avoid this method and prefer `.playSignal`! * Only use it when you want to play the sound regardless of enablement, e.g. in the settings quick pick. @@ -240,6 +240,12 @@ export class AccessibilitySignalService extends Disposable implements IAccessibi public onSoundEnabledChanged(signal: AccessibilitySignal): Event { return this.getEnabledState(signal, false).onDidChange; } + + public getDelayMs(signal: AccessibilitySignal, modality: AccessibilityModality): number { + const delaySettingsKey = signal.delaySettingsKey ?? 'accessibility.signalOptions.delays.general'; + const delaySettingsValue: { sound: number; announcement: number } = this.configurationService.getValue(delaySettingsKey); + return modality === 'sound' ? delaySettingsValue.sound : delaySettingsValue.announcement; + } } type EnabledState = 'on' | 'off' | 'auto' | 'userGesture' | 'always' | 'never'; @@ -328,6 +334,7 @@ export class AccessibilitySignal { public readonly settingsKey: string, public readonly legacyAnnouncementSettingsKey: string | undefined, public readonly announcementMessage: string | undefined, + public readonly delaySettingsKey: string | undefined ) { } private static _signals = new Set(); @@ -344,6 +351,7 @@ export class AccessibilitySignal { settingsKey: string; legacyAnnouncementSettingsKey?: string; announcementMessage?: string; + delaySettingsKey?: string; }): AccessibilitySignal { const soundSource = new SoundSource('randomOneOf' in options.sound ? options.sound.randomOneOf : [options.sound]); const signal = new AccessibilitySignal( @@ -353,6 +361,7 @@ export class AccessibilitySignal { options.settingsKey, options.legacyAnnouncementSettingsKey, options.announcementMessage, + options.delaySettingsKey ); AccessibilitySignal._signals.add(signal); return signal; @@ -367,12 +376,14 @@ export class AccessibilitySignal { sound: Sound.error, announcementMessage: localize('accessibility.signals.positionHasError', 'Error'), settingsKey: 'accessibility.signals.positionHasError', + delaySettingsKey: 'accessibility.signalOptions.delays.errorAtPosition' }); public static readonly warningAtPosition = AccessibilitySignal.register({ name: localize('accessibilitySignals.positionHasWarning.name', 'Warning at Position'), sound: Sound.warning, announcementMessage: localize('accessibility.signals.positionHasWarning', 'Warning'), settingsKey: 'accessibility.signals.positionHasWarning', + delaySettingsKey: 'accessibility.signalOptions.delays.warningAtPosition' }); public static readonly errorOnLine = AccessibilitySignal.register({ diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts index 5f36624a2ad..453b43eb916 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts @@ -187,10 +187,78 @@ const configuration: IConfigurationNode = { 'type': 'boolean', 'default': false, }, + 'delays': { + 'type': 'object', + 'additionalProperties': false, + 'properties': { + 'general': { + 'type': 'object', + 'additionalProperties': false, + 'properties': { + 'announcement': { + 'description': localize('accessibility.signalOptions.delays.general.announcement', "The delay in milliseconds before an announcement is made."), + 'type': 'number', + 'minimum': 0, + }, + 'sound': { + 'description': localize('accessibility.signalOptions.delays.general.sound', "The delay in milliseconds before a sound is played."), + 'type': 'number', + 'minimum': 0, + } + }, + }, + 'warningAtPosition': { + 'type': 'object', + 'additionalProperties': false, + 'properties': { + 'announcement': { + 'description': localize('accessibility.signalOptions.delays.warningAtPosition.announcement', "The delay in milliseconds before an announcement is made when there's a warning at the position."), + 'type': 'number', + 'minimum': 0, + }, + 'sound': { + 'description': localize('accessibility.signalOptions.delays.warningAtPosition.sound', "The delay in milliseconds before a sound is played when there's a warning at the position."), + 'type': 'number', + 'minimum': 0, + } + }, + }, + 'errorAtPosition': { + 'type': 'object', + 'additionalProperties': false, + 'properties': { + 'announcement': { + 'description': localize('accessibility.signalOptions.delays.errorAtPosition.announcement', "The delay in milliseconds before an announcement is made when there's an error at the position."), + 'type': 'number', + 'minimum': 0, + }, + 'sound': { + 'description': localize('accessibility.signalOptions.delays.errorAtPosition.sound', "The delay in milliseconds before a sound is played when there's an error at the position."), + 'type': 'number', + 'minimum': 0, + } + }, + }, + } + } }, default: { 'volume': 70, - 'debouncePositionChanges': false + 'debouncePositionChanges': false, + 'delays': { + 'general': { + 'announcement': 3000, + 'sound': 400 + }, + 'warningAtPosition': { + 'announcement': 3000, + 'sound': 1000 + }, + 'errorAtPosition': { + 'announcement': 3000, + 'sound': 1000 + } + } }, tags: ['accessibility'] }, diff --git a/src/vs/workbench/contrib/accessibilitySignals/browser/editorTextPropertySignalsContribution.ts b/src/vs/workbench/contrib/accessibilitySignals/browser/editorTextPropertySignalsContribution.ts index cd4ef189002..de1c2f798af 100644 --- a/src/vs/workbench/contrib/accessibilitySignals/browser/editorTextPropertySignalsContribution.ts +++ b/src/vs/workbench/contrib/accessibilitySignals/browser/editorTextPropertySignalsContribution.ts @@ -53,7 +53,7 @@ export class EditorTextPropertySignalsContribution extends Disposable implements constructor( @IEditorService private readonly _editorService: IEditorService, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService, + @IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService ) { super(); @@ -104,7 +104,7 @@ export class EditorTextPropertySignalsContribution extends Disposable implements for (const modality of ['sound', 'announcement'] as AccessibilityModality[]) { if (this._accessibilitySignalService.getEnabledState(signal, false, modality).value) { - const delay = this._getDelay(signal, modality) + (didType.get() ? 1000 : 0); + const delay = this._accessibilitySignalService.getDelayMs(signal, modality) + (didType.get() ? 1000 : 0); timeouts.add(disposableTimeout(() => { if (source.isPresent(position, mode, undefined)) { @@ -162,23 +162,6 @@ export class EditorTextPropertySignalsContribution extends Disposable implements } })); } - - private _getDelay(signal: AccessibilitySignal, modality: AccessibilityModality): number { - // TODO make these delays configurable! - if (signal === AccessibilitySignal.errorAtPosition || signal === AccessibilitySignal.warningAtPosition) { - if (modality === 'sound') { - return 100; - } else { - return 1000; - } - } - - if (modality === 'sound') { - return 400; - } else { - return 3000; - } - } } interface TextProperty { From 27b49d86d22a649bf80b70a41e5caad774baffd4 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Wed, 22 May 2024 09:23:30 -0700 Subject: [PATCH 317/357] fix: set `id` and `name` for files in chat context --- .../contrib/chat/browser/actions/chatContextActions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index 92a2c5f23c6..d55100c335e 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -72,9 +72,9 @@ class AttachContextAction extends Action2 { toAttach.push({ ...pick, isDynamic: pick.isDynamic, value: pick.value, name: qualifiedName, fullName: `$(${pick.icon.id}) ${selection}` }); } } else if (pick && typeof pick === 'object' && 'resource' in pick) { - toAttach.push({ ...pick, value: pick.resource }); + toAttach.push({ ...pick, value: pick.resource, name: pick.label, id: pick.resource.toString(), isDynamic: true }); } else { - toAttach.push({ ...pick, fullName: pick.label }); + toAttach.push({ ...pick, fullName: pick.label, name: 'name' in pick && typeof pick.name === 'string' ? pick.name : pick.label, icon: 'icon' in pick && ThemeIcon.isThemeIcon(pick.icon) ? pick.icon : undefined }); } } From 001b81c68342886e41d40644edb4b85543089cd7 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Wed, 22 May 2024 09:43:55 -0700 Subject: [PATCH 318/357] Disable VS Code file watching for yarn pnp (#213238) Disable VS Code file watching on yarn pnp --- .../src/tsServer/api.ts | 4 ++ .../src/tsServer/spawner.ts | 6 +- .../src/typescriptServiceClient.ts | 58 ++++++++++++------- 3 files changed, 46 insertions(+), 22 deletions(-) diff --git a/extensions/typescript-language-features/src/tsServer/api.ts b/extensions/typescript-language-features/src/tsServer/api.ts index 4a35ada0f24..2378bfb53f0 100644 --- a/extensions/typescript-language-features/src/tsServer/api.ts +++ b/extensions/typescript-language-features/src/tsServer/api.ts @@ -80,4 +80,8 @@ export class API { public lt(other: API): boolean { return !this.gte(other); } + + public isYarnPnp(): boolean { + return this.fullVersionString.includes('-sdk'); + } } diff --git a/extensions/typescript-language-features/src/tsServer/spawner.ts b/extensions/typescript-language-features/src/tsServer/spawner.ts index bc55ab4fdb3..543140dbab5 100644 --- a/extensions/typescript-language-features/src/tsServer/spawner.ts +++ b/extensions/typescript-language-features/src/tsServer/spawner.ts @@ -271,7 +271,11 @@ export class TypeScriptServerSpawner { args.push('--noGetErrOnBackgroundUpdate'); - if (apiVersion.gte(API.v544) && configuration.useVsCodeWatcher) { + if ( + apiVersion.gte(API.v544) + && configuration.useVsCodeWatcher + && !apiVersion.isYarnPnp() // Disable for yarn pnp as it currently breaks with the VS Code watcher + ) { args.push('--canUseWatchEvents'); } diff --git a/extensions/typescript-language-features/src/typescriptServiceClient.ts b/extensions/typescript-language-features/src/typescriptServiceClient.ts index 78a77f23c9f..24742f99219 100644 --- a/extensions/typescript-language-features/src/typescriptServiceClient.ts +++ b/extensions/typescript-language-features/src/typescriptServiceClient.ts @@ -5,32 +5,32 @@ import * as path from 'path'; import * as vscode from 'vscode'; +import { ServiceConfigurationProvider, SyntaxServerConfiguration, TsServerLogLevel, TypeScriptServiceConfiguration, areServiceConfigurationsEqual } from './configuration/configuration'; +import * as fileSchemes from './configuration/fileSchemes'; +import { Schemes } from './configuration/schemes'; import { IExperimentationTelemetryReporter } from './experimentTelemetryReporter'; import { DiagnosticKind, DiagnosticsManager } from './languageFeatures/diagnostics'; -import * as Proto from './tsServer/protocol/protocol'; -import { EventName } from './tsServer/protocol/protocol.const'; +import { Logger } from './logging/logger'; +import { TelemetryProperties, TelemetryReporter, VSCodeTelemetryReporter } from './logging/telemetry'; +import Tracer from './logging/tracer'; +import { ProjectType, inferredProjectCompilerOptions } from './tsconfig'; import { API } from './tsServer/api'; import BufferSyncSupport from './tsServer/bufferSyncSupport'; import { OngoingRequestCancellerFactory } from './tsServer/cancellation'; import { ILogDirectoryProvider } from './tsServer/logDirectoryProvider'; +import { NodeVersionManager } from './tsServer/nodeManager'; import { TypeScriptPluginPathsProvider } from './tsServer/pluginPathsProvider'; +import { PluginManager, TypeScriptServerPlugin } from './tsServer/plugins'; +import * as Proto from './tsServer/protocol/protocol'; +import { EventName } from './tsServer/protocol/protocol.const'; import { ITypeScriptServer, TsServerLog, TsServerProcessFactory, TypeScriptServerExitEvent } from './tsServer/server'; import { TypeScriptServerError } from './tsServer/serverError'; import { TypeScriptServerSpawner } from './tsServer/spawner'; import { TypeScriptVersionManager } from './tsServer/versionManager'; import { ITypeScriptVersionProvider, TypeScriptVersion } from './tsServer/versionProvider'; import { ClientCapabilities, ClientCapability, ExecConfig, ITypeScriptServiceClient, ServerResponse, TypeScriptRequests } from './typescriptService'; -import { ServiceConfigurationProvider, SyntaxServerConfiguration, TsServerLogLevel, TypeScriptServiceConfiguration, areServiceConfigurationsEqual } from './configuration/configuration'; import { Disposable, DisposableStore, disposeAll } from './utils/dispose'; -import * as fileSchemes from './configuration/fileSchemes'; -import { Logger } from './logging/logger'; import { isWeb, isWebAndHasSharedArrayBuffers } from './utils/platform'; -import { PluginManager, TypeScriptServerPlugin } from './tsServer/plugins'; -import { TelemetryProperties, TelemetryReporter, VSCodeTelemetryReporter } from './logging/telemetry'; -import Tracer from './logging/tracer'; -import { ProjectType, inferredProjectCompilerOptions } from './tsconfig'; -import { Schemes } from './configuration/schemes'; -import { NodeVersionManager } from './tsServer/nodeManager'; export interface TsDiagnostics { @@ -463,7 +463,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType } */ this.logTelemetry('tsserver.error'); - this.serviceExited(false); + this.serviceExited(false, apiVersion); }); handle.onExit((data: TypeScriptServerExitEvent) => { @@ -484,7 +484,6 @@ export default class TypeScriptServiceClient extends Disposable implements IType */ this.logTelemetry('tsserver.exitWithCode', { code: code ?? undefined, signal: signal ?? undefined }); - if (this.token !== mytoken) { // this is coming from an old process return; @@ -493,7 +492,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType if (handle.tsServerLog?.type === 'file') { this.info(`TSServer log file: ${handle.tsServerLog.uri.fsPath}`); } - this.serviceExited(!this.isRestarting); + this.serviceExited(!this.isRestarting, apiVersion); this.isRestarting = false; }); @@ -612,7 +611,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType }; } - private serviceExited(restart: boolean): void { + private serviceExited(restart: boolean, tsVersion: API): void { this.resetWatchers(); this.loadingIndicator.reset(); @@ -686,17 +685,34 @@ export default class TypeScriptServiceClient extends Disposable implements IType this._isPromptingAfterCrash = true; } - prompt?.then(item => { + prompt?.then(async item => { this._isPromptingAfterCrash = false; if (item === reportIssueItem) { + const minModernTsVersion = this.versionProvider.bundledVersion.apiVersion; - if ( + // Don't allow reporting issues using the PnP patched version of TS Server + if (tsVersion.isYarnPnp()) { + const reportIssue: vscode.MessageItem = { + title: vscode.l10n.t("Report issue against Yarn PnP"), + }; + const response = await vscode.window.showWarningMessage( + vscode.l10n.t("Please report an issue against Yarn PnP"), + { + modal: true, + detail: vscode.l10n.t("The workspace is using a version of the TypeScript Server that has been patched by Yarn PnP. This patching is a common source of bugs."), + }, + reportIssue); + + if (response === reportIssue) { + vscode.env.openExternal(vscode.Uri.parse('https://github.com/yarnpkg/berry/issues')); + } + } + // Don't allow reporting issues with old TS versions + else if ( minModernTsVersion && - previousState.type === ServerState.Type.Errored && - previousState.error instanceof TypeScriptServerError && - previousState.error.version.apiVersion?.lt(minModernTsVersion) + tsVersion.lt(minModernTsVersion) ) { vscode.window.showWarningMessage( vscode.l10n.t("Please update your TypeScript version"), @@ -704,7 +720,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType modal: true, detail: vscode.l10n.t( "The workspace is using an old version of TypeScript ({0}).\n\nBefore reporting an issue, please update the workspace to use TypeScript {1} or newer to make sure the bug has not already been fixed.", - previousState.error.version.apiVersion.displayName, + tsVersion.displayName, minModernTsVersion.displayName), }); } else { From a54e247fbac475f3799c72022ec694c875c64022 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 22 May 2024 18:47:03 +0200 Subject: [PATCH 319/357] Enable roaming of inflight chat requests (#213239) This enables to remove the special handling for `/explain` - fixes https://github.com/microsoft/vscode-copilot/issues/2192 - fixes https://github.com/microsoft/vscode/issues/208706 because inline chat now closes on "view in chat" - fixes https://github.com/microsoft/vscode-copilot/issues/5288 --- src/vs/base/common/lifecycle.ts | 10 ++ .../contrib/chat/common/chatModel.ts | 53 ++++-- .../contrib/chat/common/chatService.ts | 2 +- .../contrib/chat/common/chatServiceImpl.ts | 22 +++ .../chat/test/common/chatModel.test.ts | 30 ++++ .../chat/test/common/mockChatService.ts | 3 + .../inlineChat/browser/inlineChatActions.ts | 13 +- .../browser/inlineChatController.ts | 160 +++++++----------- .../browser/inlineChatSessionServiceImpl.ts | 1 - .../browser/inlineChatStrategies.ts | 26 +-- .../browser/inlineChatZoneWidget.ts | 8 +- .../contrib/inlineChat/common/inlineChat.ts | 2 - 12 files changed, 180 insertions(+), 150 deletions(-) diff --git a/src/vs/base/common/lifecycle.ts b/src/vs/base/common/lifecycle.ts index fcb5d2ec4e4..568a0124c12 100644 --- a/src/vs/base/common/lifecycle.ts +++ b/src/vs/base/common/lifecycle.ts @@ -777,6 +777,16 @@ export class DisposableMap implements ID this._store.delete(key); } + /** + * Delete the value stored for `key` from this map but return it. The caller is + * responsible for disposing of the value. + */ + deleteAndLeak(key: K): V | undefined { + const value = this._store.get(key); + this._store.delete(key); + return value; + } + keys(): IterableIterator { return this._store.keys(); } diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 500af3ab715..7338d036535 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -20,7 +20,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ILogService } from 'vs/platform/log/common/log'; import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentRequest, IChatAgentResult, IChatAgentService, reviveSerializedAgent } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatRequestTextPart, IParsedChatRequest, getPromptText, reviveParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { IChatAgentMarkdownContentWithVulnerability, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgress, IChatProgressMessage, IChatResponseProgressFileTreeData, IChatTask, IChatTextEdit, IChatTreeData, IChatUsedContext, IChatWarningMessage, ChatAgentVoteDirection, isIUsedContext } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatAgentMarkdownContentWithVulnerability, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgressMessage, IChatResponseProgressFileTreeData, IChatTask, IChatTextEdit, IChatTreeData, IChatUsedContext, IChatWarningMessage, ChatAgentVoteDirection, isIUsedContext, IChatProgress } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables'; export interface IChatRequestVariableEntry { @@ -109,9 +109,10 @@ export class ChatRequestModel implements IChatRequestModel { public response: ChatResponseModel | undefined; - private _id: string; - public get id(): string { - return this._id; + public readonly id: string; + + public get session() { + return this._session; } public get username(): string { @@ -135,12 +136,16 @@ export class ChatRequestModel implements IChatRequestModel { } constructor( - public readonly session: ChatModel, + private _session: ChatModel, public readonly message: IParsedChatRequest, private _variableData: IChatRequestVariableData, private _attempt: number = 0 ) { - this._id = 'request_' + ChatRequestModel.nextId++; + this.id = 'request_' + ChatRequestModel.nextId++; + } + + adoptTo(session: ChatModel) { + this._session = session; } } @@ -267,9 +272,10 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel private static nextId = 0; - private _id: string; - public get id(): string { - return this._id; + public readonly id: string; + + public get session() { + return this._session; } public get isComplete(): boolean { @@ -342,7 +348,7 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel constructor( _response: IMarkdownString | ReadonlyArray, - public readonly session: ChatModel, + private _session: ChatModel, private _agent: IChatAgentData | undefined, private _slashCommand: IChatAgentCommand | undefined, public readonly requestId: string, @@ -360,7 +366,7 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel this._followups = followups ? [...followups] : undefined; this._response = new Response(_response); this._register(this._response.onDidChangeValue(() => this._onDidChange.fire())); - this._id = 'response_' + ChatResponseModel.nextId++; + this.id = 'response_' + ChatResponseModel.nextId++; } /** @@ -430,6 +436,11 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel this._onDidChange.fire(); return true; } + + adoptTo(session: ChatModel) { + this._session = session; + this._onDidChange.fire(); + } } export interface IChatModel { @@ -773,6 +784,26 @@ export class ChatModel extends Disposable implements IChatModel { return request; } + adoptRequest(request: ChatRequestModel): void { + + // this doesn't use `removeRequest` because it must not dispose the request object + const oldOwner = request.session; + const index = oldOwner._requests.findIndex(candidate => candidate.id === request.id); + + if (index === -1) { + return; + } + + oldOwner._requests.splice(index, 1); + + request.adoptTo(this); + request.response?.adoptTo(this); + this._requests.push(request); + + oldOwner._onDidChange.fire({ kind: 'removeRequest', requestId: request.id, responseId: request.response?.id }); + this._onDidChange.fire({ kind: 'addRequest', request }); + } + acceptResponseProgress(request: ChatRequestModel, progress: IChatProgress, quiet?: boolean): void { if (!request.response) { request.response = new ChatResponseModel([], this, undefined, undefined, request.id); diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index f83ae8990a0..a6921314d18 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -331,7 +331,7 @@ export interface IChatService { sendRequest(sessionId: string, message: string, options?: IChatSendRequestOptions): Promise; resendRequest(request: IChatRequestModel, options?: IChatSendRequestOptions): Promise; - + adoptRequest(sessionId: string, request: IChatRequestModel): 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 4b093e11637..5ac341490c9 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -700,6 +700,28 @@ export class ChatService extends Disposable implements IChatService { model.removeRequest(requestId); } + async adoptRequest(sessionId: string, request: IChatRequestModel) { + if (!(request instanceof ChatRequestModel)) { + throw new TypeError('Can only adopt requests of type ChatRequestModel'); + } + const target = this._sessionModels.get(sessionId); + if (!target) { + throw new Error(`Unknown session: ${sessionId}`); + } + + await target.waitForInitialization(); + + const oldOwner = request.session; + target.adoptRequest(request); + + if (request.response && !request.response.isComplete) { + const cts = this._pendingRequests.deleteAndLeak(oldOwner.sessionId); + if (cts) { + this._pendingRequests.set(target.sessionId, cts); + } + } + } + async addCompleteRequest(sessionId: string, message: IParsedChatRequest | string, variableData: IChatRequestVariableData | undefined, attempt: number | undefined, response: IChatCompleteResponse): Promise { this.trace('addCompleteRequest', `message: ${message}`); 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 f611d83b246..b9f38a04549 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts @@ -121,6 +121,36 @@ suite('ChatModel', () => { model.removeRequest(requests[0].id); assert.strictEqual(model.getRequests().length, 0); }); + + test('adoptRequest', async function () { + const model1 = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, ChatAgentLocation.Editor)); + const model2 = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, ChatAgentLocation.Panel)); + + model1.startInitialize(); + model1.initialize(undefined); + + model2.startInitialize(); + model2.initialize(undefined); + + const text = 'hello'; + const request1 = model1.addRequest({ text, parts: [new ChatRequestTextPart(new OffsetRange(0, text.length), new Range(1, text.length, 1, text.length), text)] }, { variables: [] }, 0); + + assert.strictEqual(model1.getRequests().length, 1); + assert.strictEqual(model2.getRequests().length, 0); + assert.ok(request1.session === model1); + assert.ok(request1.response?.session === model1); + + model2.adoptRequest(request1); + + assert.strictEqual(model1.getRequests().length, 0); + assert.strictEqual(model2.getRequests().length, 1); + assert.ok(request1.session === model2); + assert.ok(request1.response?.session === model2); + + model2.acceptResponseProgress(request1, { content: new MarkdownString('Hello'), kind: 'markdownContent' }); + + assert.strictEqual(request1.response.response.asString(), 'Hello'); + }); }); suite('Response', () => { diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts index f2671c73f59..c7cc9199757 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts @@ -46,6 +46,9 @@ export class MockChatService implements IChatService { resendRequest(request: IChatRequestModel, options?: IChatSendRequestOptions | undefined): Promise { throw new Error('Method not implemented.'); } + adoptRequest(sessionId: string, request: IChatRequestModel): Promise { + throw new Error('Method not implemented.'); + } removeRequest(sessionid: string, requestId: string): Promise { throw new Error('Method not implemented.'); } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index 472bd45cf46..ce5b514ede3 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -29,6 +29,7 @@ import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { ILogService } from 'vs/platform/log/common/log'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; +import { CONTEXT_CHAT_REQUEST_IN_PROGRESS } from 'vs/workbench/contrib/chat/common/chatContextKeys'; CommandsRegistry.registerCommandAlias('interactiveEditor.start', 'inlineChat.start'); CommandsRegistry.registerCommandAlias('interactive.acceptChanges', ACTION_ACCEPT_CHANGES); @@ -482,15 +483,14 @@ export class ViewInChatAction extends AbstractInlineChatAction { icon: Codicon.commentDiscussion, precondition: CTX_INLINE_CHAT_VISIBLE, menu: { - id: MENU_INLINE_CHAT_WIDGET_STATUS, - when: CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.OnlyMessages), - group: '0_main', - order: 1 + id: MENU_INLINE_CHAT_WIDGET, + group: 'navigation', + order: 5 } }); } - override runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: any[]): void { - ctrl.viewInChat(); + override runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: any[]) { + return ctrl.viewInChat(); } } @@ -501,6 +501,7 @@ export class RerunAction extends AbstractInlineChatAction { title: localize2('chat.rerun.label', "Rerun Request"), f1: false, icon: Codicon.refresh, + precondition: CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate(), menu: { id: MENU_INLINE_CHAT_WIDGET_STATUS, group: '0_main', diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index bac8e7419f3..98a8f4294a5 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -29,8 +29,6 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { IChatWidgetService, showChatView } from 'vs/workbench/contrib/chat/browser/chat'; -import { ChatAgentLocation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { chatAgentLeader, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { IInlineChatSavingService } from './inlineChatSavingService'; import { EmptyResponse, ErrorResponse, ReplyResponse, Session, SessionPrompt } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; @@ -42,12 +40,10 @@ import { StashedSession } from './inlineChatSession'; import { IModelDeltaDecoration, ITextModel, IValidEditOperation } from 'vs/editor/common/model'; import { InlineChatContentWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget'; import { MessageController } from 'vs/editor/contrib/message/browser/messageController'; -import { tail } from 'vs/base/common/arrays'; -import { IChatRequestModel, IResponse } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatModel, IChatRequestModel, IResponse } from 'vs/workbench/contrib/chat/common/chatModel'; import { InlineChatError } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart'; -import { OffsetRange } from 'vs/editor/common/core/offsetRange'; import { isEqual } from 'vs/base/common/resources'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; @@ -144,7 +140,6 @@ export class InlineChatController implements IEditorContribution { @IConfigurationService private readonly _configurationService: IConfigurationService, @IDialogService private readonly _dialogService: IDialogService, @IContextKeyService contextKeyService: IContextKeyService, - @IChatAgentService private readonly _chatAgentService: IChatAgentService, @IChatService private readonly _chatService: IChatService, @ILanguageFeaturesService private readonly _languageFeatureService: ILanguageFeaturesService, @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, @@ -428,14 +423,14 @@ export class InlineChatController implements IEditorContribution { }); } else if (e.kind === 'removeRequest') { // TODO@jrieken this currently is buggy when removing not the very last request/response - if (this._session!.lastExchange?.response instanceof ReplyResponse) { - try { - this._session!.hunkData.ignoreTextModelNChanges = true; - await this._strategy!.undoChanges(this._session!.lastExchange.response.modelAltVersionId); - } finally { - this._session!.hunkData.ignoreTextModelNChanges = false; - } - } + // if (this._session!.lastExchange?.response instanceof ReplyResponse) { + // try { + // this._session!.hunkData.ignoreTextModelNChanges = true; + // await this._strategy!.undoChanges(this._session!.lastExchange.response.modelAltVersionId); + // } finally { + // this._session!.hunkData.ignoreTextModelNChanges = false; + // } + // } } })); @@ -468,7 +463,6 @@ export class InlineChatController implements IEditorContribution { kind: CompletionItemKind.Text, insertText: withSlash, range: Range.fromPositions(new Position(1, 1), position), - command: command.executeImmediately ? { id: 'workbench.action.chat.acceptInput', title: withSlash } : undefined }); } @@ -599,55 +593,18 @@ export class InlineChatController implements IEditorContribution { const input = request.message.text; this._zone.value.widget.value = input; - // slash command referring - let slashCommandLike = request.message.parts.find(part => part instanceof ChatRequestAgentSubcommandPart || part instanceof ChatRequestSlashCommandPart); - const refer = this._session.session.slashCommands?.some(value => { - if (value.refer) { - if (slashCommandLike?.text === `/${value.command}`) { - return true; - } - if (request?.message.text.startsWith(`/${value.command}`)) { - slashCommandLike = new ChatRequestSlashCommandPart(new OffsetRange(0, 1), new Range(1, 1, 1, 1), { command: value.command, detail: value.detail ?? '' }); - return true; - } - } - return false; - }); - if (refer && slashCommandLike && !this._session.lastExchange) { - this._log('[IE] seeing refer command, continuing outside editor', this._session.agent.extensionId); - - // cancel this request - this._chatService.cancelCurrentRequestForSession(request.session.sessionId); - - this._editor.setSelection(this._session.wholeRange.value); - let massagedInput = input; - const withoutSubCommandLeader = slashCommandLike.text.slice(1); - for (const agent of this._chatAgentService.getActivatedAgents()) { - if (agent.locations.includes(ChatAgentLocation.Panel)) { - const commands = agent.slashCommands; - if (commands.find((command) => withoutSubCommandLeader.startsWith(command.name))) { - massagedInput = `${chatAgentLeader}${agent.name} ${slashCommandLike.text}`; - break; - } - } - } - // if agent has a refer command, massage the input to include the agent name - await this._instaService.invokeFunction(sendRequest, massagedInput); - - return State.ACCEPT; - } - this._session.addInput(new SessionPrompt(request)); return State.SHOW_REQUEST; } - private async [State.SHOW_REQUEST](): Promise { + private async [State.SHOW_REQUEST](): Promise { assertType(this._session); assertType(this._session.chatModel.requestInProgress); - const request: IChatRequestModel | undefined = tail(this._session.chatModel.getRequests()); + const { chatModel } = this._session; + const request: IChatRequestModel | undefined = chatModel.getRequests().at(-1); assertType(request); assertType(request.response); @@ -667,18 +624,30 @@ export class InlineChatController implements IEditorContribution { const progressiveEditsClock = StopWatch.create(); const progressiveEditsQueue = new Queue(); + let next: State.SHOW_RESPONSE | State.CANCEL | State.PAUSE | State.ACCEPT | State.WAIT_FOR_INPUT = State.SHOW_RESPONSE; + store.add(Event.once(this._messages.event)(message => { + this._log('state=_makeRequest) message received', message); + this._chatService.cancelCurrentRequestForSession(chatModel.sessionId); + if (message & Message.CANCEL_SESSION) { + next = State.CANCEL; + } else if (message & Message.PAUSE_SESSION) { + next = State.PAUSE; + } else if (message & Message.ACCEPT_SESSION) { + next = State.ACCEPT; + } + })); - - let message = Message.NONE; - store.add(Event.once(this._messages.event)(m => { - this._log('state=_makeRequest) message received', m); - this._chatService.cancelCurrentRequestForSession(request.session.sessionId); - message = m; + store.add(chatModel.onDidChange(e => { + if (e.kind === 'removeRequest' && e.requestId === request.id) { + progressiveEditsCts.cancel(); + responsePromise.complete(); + next = State.CANCEL; + } })); // cancel the request when the user types store.add(this._zone.value.widget.chatWidget.inputEditor.onDidChangeModelContent(() => { - this._chatService.cancelCurrentRequestForSession(request.session.sessionId); + this._chatService.cancelCurrentRequestForSession(chatModel.sessionId); })); let lastLength = 0; @@ -754,16 +723,9 @@ export class InlineChatController implements IEditorContribution { await this._session.hunkData.recompute(); this._zone.value.widget.updateToolbar(true); + this._zone.value.widget.updateProgress(false); - if (message & Message.CANCEL_SESSION) { - return State.CANCEL; - } else if (message & Message.PAUSE_SESSION) { - return State.PAUSE; - } else if (message & Message.ACCEPT_SESSION) { - return State.ACCEPT; - } else { - return State.SHOW_RESPONSE; - } + return next; } private async[State.SHOW_RESPONSE](): Promise { @@ -1040,11 +1002,21 @@ export class InlineChatController implements IEditorContribution { this._strategy?.move?.(next); } - - viewInChat() { - if (this._session?.lastExchange?.response instanceof ReplyResponse) { - this._instaService.invokeFunction(showMessageResponse, this._session.lastExchange.prompt.value, this._session.lastExchange.response.mdContent.value); + async viewInChat() { + if (!this._strategy || !this._session) { + return; } + + // TODO@jrieken REMOVE this as soon as we can mark responses as accepted + // and as soon as hunks support request-linking + const textEditsResponseCount = this._session.chatModel.getRequests().filter(request => request.response?.response.value.some(part => part.kind === 'textEditGroup')).length; + if (textEditsResponseCount > 1) { + return; + } + + this._strategy.cancel(); + await this._instaService.invokeFunction(moveToPanelChat, this._session?.chatModel); + this.cancelSession(); } toggleDiff() { @@ -1061,7 +1033,7 @@ export class InlineChatController implements IEditorContribution { if (this._session?.lastExchange?.response instanceof ReplyResponse && this._session?.lastExchange?.response.chatResponse) { const response = this._session?.lastExchange?.response.chatResponse; this._chatService.notifyUserAction({ - sessionId: response.session.sessionId, + sessionId: this._session.chatModel.sessionId, requestId: response.requestId, agentId: response.agent?.id, result: response.result, @@ -1093,7 +1065,7 @@ export class InlineChatController implements IEditorContribution { if (this._session.lastExchange?.response instanceof ReplyResponse && this._session?.lastExchange?.response.chatResponse) { const response = this._session?.lastExchange?.response.chatResponse; this._chatService.notifyUserAction({ - sessionId: response.session.sessionId, + sessionId: this._session.chatModel.sessionId, requestId: response.requestId, agentId: response.agent?.id, result: response.result, @@ -1134,35 +1106,21 @@ export class InlineChatController implements IEditorContribution { } } -async function showMessageResponse(accessor: ServicesAccessor, query: string, response: string) { - const chatService = accessor.get(IChatService); - const chatAgentService = accessor.get(IChatAgentService); - const agent = chatAgentService.getActivatedAgents().find(agent => agent.locations.includes(ChatAgentLocation.Panel) && agent.isDefault); - if (!agent) { - return; - } +async function moveToPanelChat(accessor: ServicesAccessor, model: ChatModel | undefined) { - const widget = await showChatView(accessor.get(IViewsService)); - if (widget && widget.viewModel) { - chatService.addCompleteRequest(widget.viewModel.sessionId, query, undefined, 0, { message: response }); + const viewsService = accessor.get(IViewsService); + const chatService = accessor.get(IChatService); + + const widget = await showChatView(viewsService); + + if (widget && widget.viewModel && model) { + for (const request of model.getRequests().slice()) { + await chatService.adoptRequest(widget.viewModel.model.sessionId, request); + } widget.focusLastMessage(); } } -async function sendRequest(accessor: ServicesAccessor, query: string) { - const chatAgentService = accessor.get(IChatAgentService); - const agent = chatAgentService.getActivatedAgents().find(agent => agent.locations.includes(ChatAgentLocation.Panel) && agent.isDefault); - if (!agent) { - return; - } - const widget = await showChatView(accessor.get(IViewsService)); - if (!widget) { - return; - } - widget.focusInput(); - widget.acceptInput(query); -} - function asInlineChatResponseType(response: IResponse): InlineChatResponseTypes { let result: InlineChatResponseTypes | undefined; for (const item of response.value) { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts index 7e77096a55c..36741784114 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts @@ -154,7 +154,6 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { return { command: agentCommand.name, detail: agentCommand.description, - refer: agentCommand.name === 'explain' // TODO@jrieken @joyceerhl this should be cleaned up } satisfies IInlineChatSlashCommand; }) }; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts index 2f1e2e33266..458c0da69a6 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts @@ -17,7 +17,7 @@ import { LineRange } from 'vs/editor/common/core/lineRange'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { IEditorDecorationsCollection } from 'vs/editor/common/editorCommon'; -import { IModelDecorationsChangeAccessor, IModelDeltaDecoration, ITextModel, IValidEditOperation, MinimapPosition, OverviewRulerLane, TrackedRangeStickiness } from 'vs/editor/common/model'; +import { IModelDecorationsChangeAccessor, IModelDeltaDecoration, IValidEditOperation, MinimapPosition, OverviewRulerLane, TrackedRangeStickiness } from 'vs/editor/common/model'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { InlineDecoration, InlineDecorationType } from 'vs/editor/common/viewModel'; @@ -136,8 +136,6 @@ export abstract class EditModeStrategy { abstract makeChanges(edits: ISingleEditOperation[], obs: IEditObserver, undoStopBefore: boolean): Promise; - abstract undoChanges(altVersionId: number): Promise; - abstract renderChanges(response: ReplyResponse): Promise; move?(next: boolean): void; @@ -192,14 +190,7 @@ export class PreviewStrategy extends EditModeStrategy { override async makeProgressiveChanges(): Promise { } - override async undoChanges(altVersionId: number): Promise { - const { textModelN } = this._session; - await undoModelUntil(textModelN, altVersionId); - } - - override async renderChanges(response: ReplyResponse): Promise { - - } + override async renderChanges(response: ReplyResponse): Promise { } hasFocus(): boolean { return this._zone.widget.hasFocus(); @@ -313,11 +304,6 @@ export class LiveStrategy extends EditModeStrategy { return super.cancel(); } - override async undoChanges(altVersionId: number): Promise { - const { textModelN } = this._session; - await undoModelUntil(textModelN, altVersionId); - } - override async makeChanges(edits: ISingleEditOperation[], obs: IEditObserver, undoStopBefore: boolean): Promise { return this._makeChanges(edits, obs, undefined, undefined, undoStopBefore); } @@ -618,14 +604,6 @@ export class LiveStrategy extends EditModeStrategy { } } - -async function undoModelUntil(model: ITextModel, targetAltVersion: number): Promise { - while (targetAltVersion < model.getAlternativeVersionId() && model.canUndo()) { - await model.undo(); - } -} - - function changeDecorationsAndViewZones(editor: ICodeEditor, callback: (accessor: IModelDecorationsChangeAccessor, viewZoneAccessor: IViewZoneChangeAccessor) => void): void { editor.changeDecorations(decorationsAccessor => { editor.changeViewZones(viewZoneAccessor => { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts index d37ea30b024..5dcca5cbb31 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts @@ -14,7 +14,7 @@ import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/browser/zoneWidget'; import { localize } from 'vs/nls'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ACTION_ACCEPT_CHANGES, ACTION_REGENERATE_RESPONSE, ACTION_TOGGLE_DIFF, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, EditMode, InlineChatConfigKeys, MENU_INLINE_CHAT_WIDGET, MENU_INLINE_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { ACTION_ACCEPT_CHANGES, ACTION_REGENERATE_RESPONSE, ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, EditMode, InlineChatConfigKeys, MENU_INLINE_CHAT_WIDGET, MENU_INLINE_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { EditorBasedInlineChatWidget } from './inlineChatWidget'; import { MenuId } from 'vs/platform/actions/common/actions'; import { isEqual } from 'vs/base/common/resources'; @@ -53,9 +53,9 @@ export class InlineChatZoneWidget extends ZoneWidget { menu: MENU_INLINE_CHAT_WIDGET_STATUS, options: { buttonConfigProvider: action => { - if (action.id === ACTION_REGENERATE_RESPONSE || action.id === ACTION_TOGGLE_DIFF) { - return { showIcon: true, showLabel: false, isSecondary: true }; - } else if (action.id === ACTION_VIEW_IN_CHAT || action.id === ACTION_ACCEPT_CHANGES) { + if (new Set([ACTION_REGENERATE_RESPONSE, ACTION_TOGGLE_DIFF]).has(action.id)) { + return { isSecondary: true, showIcon: true, showLabel: false }; + } else if (action.id === ACTION_ACCEPT_CHANGES) { return { isSecondary: false }; } else { return { isSecondary: true }; diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index 37beed15c3a..2ee3979e13d 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -23,8 +23,6 @@ import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; export interface IInlineChatSlashCommand { command: string; detail?: string; - refer?: boolean; - executeImmediately?: boolean; } export interface IInlineChatSession { From 4ebc77f80aeb8faf6d9aac0226406f7114d2d4ec Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Wed, 22 May 2024 09:48:44 -0700 Subject: [PATCH 320/357] Add VS device id (#213231) --- package.json | 1 + src/vs/base/node/id.ts | 11 ++++++ src/vs/base/test/node/id.test.ts | 9 ++++- src/vs/code/electron-main/app.ts | 21 +++++------ src/vs/code/node/cliProcessMain.ts | 5 +-- .../node/sharedProcess/sharedProcessMain.ts | 2 +- .../standalone/browser/standaloneServices.ts | 1 + .../electron-main/sharedProcess.ts | 2 ++ .../sharedProcess/node/sharedProcess.ts | 2 ++ .../telemetry/common/commonProperties.ts | 3 ++ src/vs/platform/telemetry/common/telemetry.ts | 2 ++ .../telemetry/common/telemetryService.ts | 2 ++ .../telemetry/common/telemetryUtils.ts | 1 + .../telemetry/electron-main/telemetryUtils.ts | 10 ++++-- .../platform/telemetry/node/telemetryUtils.ts | 13 +++++-- src/vs/platform/window/common/window.ts | 1 + .../electron-main/windowsMainService.ts | 2 ++ src/vs/server/node/serverServices.ts | 9 ++--- .../workbench/api/common/extHostTelemetry.ts | 1 + .../api/test/browser/extHostTelemetry.test.ts | 3 +- .../electron-sandbox/environmentService.ts | 4 +++ .../browser/webWorkerExtensionHost.ts | 1 + .../common/extensionHostProtocol.ts | 1 + .../extensions/common/remoteExtensionHost.ts | 1 + .../localProcessExtensionHost.ts | 1 + .../telemetry/browser/telemetryService.ts | 1 + .../common/workbenchCommonProperties.ts | 3 +- .../electron-sandbox/telemetryService.ts | 3 +- .../test/node/commonProperties.test.ts | 6 ++-- .../workingCopyBackupService.test.ts | 1 + yarn.lock | 36 +++++++++++++++++++ 31 files changed, 131 insertions(+), 28 deletions(-) diff --git a/package.json b/package.json index 2f22c3ef924..5c1633fce10 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "2.1.0", + "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/policy-watcher": "^1.1.4", "@vscode/proxy-agent": "^0.19.0", diff --git a/src/vs/base/node/id.ts b/src/vs/base/node/id.ts index 42a7f771358..5f0fb74aefd 100644 --- a/src/vs/base/node/id.ts +++ b/src/vs/base/node/id.ts @@ -114,3 +114,14 @@ export async function getSqmMachineId(errorLogger: (error: any) => void): Promis } return ''; } + +export async function getVSDeviceId(errorLogger: (error: any) => void): Promise { + try { + const deviceIdPackage = await import('@vscode/deviceid'); + const id = await deviceIdPackage.getDeviceId(); + return id; + } catch (err) { + errorLogger(err); + return ''; + } +} diff --git a/src/vs/base/test/node/id.test.ts b/src/vs/base/test/node/id.test.ts index 47580ff957c..3c733d92989 100644 --- a/src/vs/base/test/node/id.test.ts +++ b/src/vs/base/test/node/id.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { getMachineId, getSqmMachineId } from 'vs/base/node/id'; +import { getMachineId, getSqmMachineId, getVSDeviceId } from 'vs/base/node/id'; import { getMac } from 'vs/base/node/macAddress'; import { flakySuite } from 'vs/base/test/node/testUtils'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; @@ -26,6 +26,13 @@ flakySuite('ID', () => { assert.strictEqual(errors.length, 0); }); + test('getVSDeviceId', async function () { + const errors = []; + const id = await getVSDeviceId(err => errors.push(err)); + assert.ok(typeof id === 'string'); + assert.strictEqual(errors.length, 0); + }); + test('getMac', async () => { const macAddress = getMac(); assert.ok(/^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/.test(macAddress), `Expected a MAC address, got: ${macAddress}`); diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index d19e8bfe49b..a56ef86fb2f 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -105,7 +105,7 @@ import { ExtensionsScannerService } from 'vs/platform/extensionManagement/node/e import { UserDataProfilesHandler } from 'vs/platform/userDataProfile/electron-main/userDataProfilesHandler'; import { ProfileStorageChangesListenerChannel } from 'vs/platform/userDataProfile/electron-main/userDataProfileStorageIpc'; import { Promises, RunOnceScheduler, runWhenGlobalIdle } from 'vs/base/common/async'; -import { resolveMachineId, resolveSqmId } from 'vs/platform/telemetry/electron-main/telemetryUtils'; +import { resolveMachineId, resolveSqmId, resolveVSDeviceId } from 'vs/platform/telemetry/electron-main/telemetryUtils'; import { ExtensionsProfileScannerService } from 'vs/platform/extensionManagement/node/extensionsProfileScannerService'; import { LoggerChannel } from 'vs/platform/log/electron-main/logIpc'; import { ILoggerMainService } from 'vs/platform/log/electron-main/loggerService'; @@ -611,17 +611,18 @@ export class CodeApplication extends Disposable { // Resolve unique machine ID this.logService.trace('Resolving machine identifier...'); - const [machineId, sqmId] = await Promise.all([ + const [machineId, sqmId, vsDeviceId] = await Promise.all([ resolveMachineId(this.stateService, this.logService), - resolveSqmId(this.stateService, this.logService) + resolveSqmId(this.stateService, this.logService), + resolveVSDeviceId(this.stateService, this.logService) ]); this.logService.trace(`Resolved machine identifier: ${machineId}`); // Shared process - const { sharedProcessReady, sharedProcessClient } = this.setupSharedProcess(machineId, sqmId); + const { sharedProcessReady, sharedProcessClient } = this.setupSharedProcess(machineId, sqmId, vsDeviceId); // Services - const appInstantiationService = await this.initServices(machineId, sqmId, sharedProcessReady); + const appInstantiationService = await this.initServices(machineId, sqmId, vsDeviceId, sharedProcessReady); // Auth Handler this._register(appInstantiationService.createInstance(ProxyAuthHandler)); @@ -986,8 +987,8 @@ export class CodeApplication extends Disposable { return false; } - private setupSharedProcess(machineId: string, sqmId: string): { sharedProcessReady: Promise; sharedProcessClient: Promise } { - const sharedProcess = this._register(this.mainInstantiationService.createInstance(SharedProcess, machineId, sqmId)); + private setupSharedProcess(machineId: string, sqmId: string, vsDeviceId: string): { sharedProcessReady: Promise; sharedProcessClient: Promise } { + const sharedProcess = this._register(this.mainInstantiationService.createInstance(SharedProcess, machineId, sqmId, vsDeviceId)); this._register(sharedProcess.onDidCrash(() => this.windowsMainService?.sendToFocused('vscode:reportSharedProcessCrash'))); @@ -1010,7 +1011,7 @@ export class CodeApplication extends Disposable { return { sharedProcessReady, sharedProcessClient }; } - private async initServices(machineId: string, sqmId: string, sharedProcessReady: Promise): Promise { + private async initServices(machineId: string, sqmId: string, vsDeviceId: string, sharedProcessReady: Promise): Promise { const services = new ServiceCollection(); // Update @@ -1033,7 +1034,7 @@ export class CodeApplication extends Disposable { } // Windows - services.set(IWindowsMainService, new SyncDescriptor(WindowsMainService, [machineId, sqmId, this.userEnv], false)); + services.set(IWindowsMainService, new SyncDescriptor(WindowsMainService, [machineId, sqmId, vsDeviceId, this.userEnv], false)); services.set(IAuxiliaryWindowsMainService, new SyncDescriptor(AuxiliaryWindowsMainService, undefined, false)); // Dialogs @@ -1113,7 +1114,7 @@ export class CodeApplication extends Disposable { const isInternal = isInternalTelemetry(this.productService, this.configurationService); const channel = getDelayedChannel(sharedProcessReady.then(client => client.getChannel('telemetryAppender'))); const appender = new TelemetryAppenderClient(channel); - const commonProperties = resolveCommonProperties(release(), hostname(), process.arch, this.productService.commit, this.productService.version, machineId, sqmId, isInternal); + const commonProperties = resolveCommonProperties(release(), hostname(), process.arch, this.productService.commit, this.productService.version, machineId, sqmId, vsDeviceId, isInternal); const piiPaths = getPiiPathsFromEnvironment(this.environmentMainService); const config: ITelemetryServiceConfig = { appenders: [appender], commonProperties, piiPaths, sendErrorTelemetry: true }; diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index aea83578ee0..62974272d38 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -57,7 +57,7 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { UserDataProfilesReadonlyService } from 'vs/platform/userDataProfile/node/userDataProfile'; -import { resolveMachineId, resolveSqmId } from 'vs/platform/telemetry/node/telemetryUtils'; +import { resolveMachineId, resolveSqmId, resolveVSDeviceId } from 'vs/platform/telemetry/node/telemetryUtils'; import { ExtensionsProfileScannerService } from 'vs/platform/extensionManagement/node/extensionsProfileScannerService'; import { LogService } from 'vs/platform/log/common/logService'; import { LoggerService } from 'vs/platform/log/node/loggerService'; @@ -186,6 +186,7 @@ class CliMain extends Disposable { } } const sqmId = await resolveSqmId(stateService, logService); + const vsDeviceId = await resolveVSDeviceId(stateService, logService); // Initialize user data profiles after initializing the state userDataProfilesService.init(); @@ -221,7 +222,7 @@ class CliMain extends Disposable { const config: ITelemetryServiceConfig = { appenders, sendErrorTelemetry: false, - commonProperties: resolveCommonProperties(release(), hostname(), process.arch, productService.commit, productService.version, machineId, sqmId, isInternal), + commonProperties: resolveCommonProperties(release(), hostname(), process.arch, productService.commit, productService.version, machineId, sqmId, vsDeviceId, isInternal), piiPaths: getPiiPathsFromEnvironment(environmentService) }; diff --git a/src/vs/code/node/sharedProcess/sharedProcessMain.ts b/src/vs/code/node/sharedProcess/sharedProcessMain.ts index 9a59ccce5c3..89d14e56747 100644 --- a/src/vs/code/node/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/node/sharedProcess/sharedProcessMain.ts @@ -307,7 +307,7 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter { telemetryService = new TelemetryService({ appenders, - commonProperties: resolveCommonProperties(release(), hostname(), process.arch, productService.commit, productService.version, this.configuration.machineId, this.configuration.sqmId, internalTelemetry), + commonProperties: resolveCommonProperties(release(), hostname(), process.arch, productService.commit, productService.version, this.configuration.machineId, this.configuration.sqmId, this.configuration.vsDeviceId, internalTelemetry), sendErrorTelemetry: true, piiPaths: getPiiPathsFromEnvironment(environmentService), }, configurationService, productService); diff --git a/src/vs/editor/standalone/browser/standaloneServices.ts b/src/vs/editor/standalone/browser/standaloneServices.ts index 310436b568f..fb624c2b126 100644 --- a/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/src/vs/editor/standalone/browser/standaloneServices.ts @@ -792,6 +792,7 @@ class StandaloneTelemetryService implements ITelemetryService { readonly sessionId = 'someValue.sessionId'; readonly machineId = 'someValue.machineId'; readonly sqmId = 'someValue.sqmId'; + readonly vsDeviceId = 'someValue.vsDeviceId'; readonly firstSessionDate = 'someValue.firstSessionDate'; readonly sendErrorTelemetry = false; setEnabled(): void { } diff --git a/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts b/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts index 21038e9dc86..b94f362da38 100644 --- a/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts +++ b/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts @@ -34,6 +34,7 @@ export class SharedProcess extends Disposable { constructor( private readonly machineId: string, private readonly sqmId: string, + private readonly vsDeviceId: string, @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, @@ -180,6 +181,7 @@ export class SharedProcess extends Disposable { return { machineId: this.machineId, sqmId: this.sqmId, + vsDeviceId: this.vsDeviceId, codeCachePath: this.environmentMainService.codeCachePath, profiles: { home: this.userDataProfilesService.profilesHome, diff --git a/src/vs/platform/sharedProcess/node/sharedProcess.ts b/src/vs/platform/sharedProcess/node/sharedProcess.ts index f93082d7a2d..df9d68f70b2 100644 --- a/src/vs/platform/sharedProcess/node/sharedProcess.ts +++ b/src/vs/platform/sharedProcess/node/sharedProcess.ts @@ -15,6 +15,8 @@ export interface ISharedProcessConfiguration { readonly sqmId: string; + readonly vsDeviceId: string; + readonly codeCachePath: string | undefined; readonly args: NativeParsedArgs; diff --git a/src/vs/platform/telemetry/common/commonProperties.ts b/src/vs/platform/telemetry/common/commonProperties.ts index 9ae3427a07b..d774c4f2f32 100644 --- a/src/vs/platform/telemetry/common/commonProperties.ts +++ b/src/vs/platform/telemetry/common/commonProperties.ts @@ -24,6 +24,7 @@ export function resolveCommonProperties( version: string | undefined, machineId: string | undefined, sqmId: string | undefined, + vsDeviceId: string | undefined, isInternalTelemetry: boolean, product?: string ): ICommonProperties { @@ -33,6 +34,8 @@ export function resolveCommonProperties( result['common.machineId'] = machineId; // __GDPR__COMMON__ "common.sqmId" : { "endPoint": "SqmMachineId", "classification": "EndUserPseudonymizedInformation", "purpose": "BusinessInsight" } result['common.sqmId'] = sqmId; + // __GDPR__COMMON__ "common.vsDeviceId" : { "endPoint": "SqmMachineId", "classification": "EndUserPseudonymizedInformation", "purpose": "BusinessInsight" } + result['common.vsDeviceId'] = vsDeviceId; // __GDPR__COMMON__ "sessionID" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } result['sessionID'] = generateUuid() + Date.now(); // __GDPR__COMMON__ "commitHash" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } diff --git a/src/vs/platform/telemetry/common/telemetry.ts b/src/vs/platform/telemetry/common/telemetry.ts index 73db745352a..98acd610cee 100644 --- a/src/vs/platform/telemetry/common/telemetry.ts +++ b/src/vs/platform/telemetry/common/telemetry.ts @@ -23,6 +23,7 @@ export interface ITelemetryService { readonly sessionId: string; readonly machineId: string; readonly sqmId: string; + readonly vsDeviceId: string; readonly firstSessionDate: string; readonly msftInternal?: boolean; @@ -73,6 +74,7 @@ export const firstSessionDateStorageKey = 'telemetry.firstSessionDate'; export const lastSessionDateStorageKey = 'telemetry.lastSessionDate'; export const machineIdKey = 'telemetry.machineId'; export const sqmIdKey = 'telemetry.sqmId'; +export const vsDeviceIdKey = 'telemetry.vsDeviceId'; // Configuration Keys export const TELEMETRY_SECTION_ID = 'telemetry'; diff --git a/src/vs/platform/telemetry/common/telemetryService.ts b/src/vs/platform/telemetry/common/telemetryService.ts index a9a9cc48109..131f6e1b61f 100644 --- a/src/vs/platform/telemetry/common/telemetryService.ts +++ b/src/vs/platform/telemetry/common/telemetryService.ts @@ -34,6 +34,7 @@ export class TelemetryService implements ITelemetryService { readonly sessionId: string; readonly machineId: string; readonly sqmId: string; + readonly vsDeviceId: string; readonly firstSessionDate: string; readonly msftInternal: boolean | undefined; @@ -58,6 +59,7 @@ export class TelemetryService implements ITelemetryService { this.sessionId = this._commonProperties['sessionID'] as string; this.machineId = this._commonProperties['common.machineId'] as string; this.sqmId = this._commonProperties['common.sqmId'] as string; + this.vsDeviceId = this._commonProperties['common.vsDeviceId'] as string; this.firstSessionDate = this._commonProperties['common.firstSessionDate'] as string; this.msftInternal = this._commonProperties['common.msftInternal'] as boolean | undefined; diff --git a/src/vs/platform/telemetry/common/telemetryUtils.ts b/src/vs/platform/telemetry/common/telemetryUtils.ts index c26c33886cc..45a8242943a 100644 --- a/src/vs/platform/telemetry/common/telemetryUtils.ts +++ b/src/vs/platform/telemetry/common/telemetryUtils.ts @@ -30,6 +30,7 @@ export class NullTelemetryServiceShape implements ITelemetryService { readonly sessionId = 'someValue.sessionId'; readonly machineId = 'someValue.machineId'; readonly sqmId = 'someValue.sqmId'; + readonly vsDeviceId = 'someValue.vsDeviceId'; readonly firstSessionDate = 'someValue.firstSessionDate'; readonly sendErrorTelemetry = false; publicLog() { } diff --git a/src/vs/platform/telemetry/electron-main/telemetryUtils.ts b/src/vs/platform/telemetry/electron-main/telemetryUtils.ts index 6dc9a9fa9d6..74916a938d1 100644 --- a/src/vs/platform/telemetry/electron-main/telemetryUtils.ts +++ b/src/vs/platform/telemetry/electron-main/telemetryUtils.ts @@ -5,8 +5,8 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IStateService } from 'vs/platform/state/node/state'; -import { machineIdKey, sqmIdKey } from 'vs/platform/telemetry/common/telemetry'; -import { resolveMachineId as resolveNodeMachineId, resolveSqmId as resolveNodeSqmId } from 'vs/platform/telemetry/node/telemetryUtils'; +import { machineIdKey, sqmIdKey, vsDeviceIdKey } from 'vs/platform/telemetry/common/telemetry'; +import { resolveMachineId as resolveNodeMachineId, resolveSqmId as resolveNodeSqmId, resolveVSDeviceId as resolveNodeVSDeviceId } from 'vs/platform/telemetry/node/telemetryUtils'; export async function resolveMachineId(stateService: IStateService, logService: ILogService): Promise { // Call the node layers implementation to avoid code duplication @@ -20,3 +20,9 @@ export async function resolveSqmId(stateService: IStateService, logService: ILog stateService.setItem(sqmIdKey, sqmId); return sqmId; } + +export async function resolveVSDeviceId(stateService: IStateService, logService: ILogService): Promise { + const vsDeviceId = await resolveNodeVSDeviceId(stateService, logService); + stateService.setItem(vsDeviceIdKey, vsDeviceId); + return vsDeviceId; +} diff --git a/src/vs/platform/telemetry/node/telemetryUtils.ts b/src/vs/platform/telemetry/node/telemetryUtils.ts index cb5a03fd687..836b2925ca7 100644 --- a/src/vs/platform/telemetry/node/telemetryUtils.ts +++ b/src/vs/platform/telemetry/node/telemetryUtils.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { isMacintosh } from 'vs/base/common/platform'; -import { getMachineId, getSqmMachineId } from 'vs/base/node/id'; +import { getMachineId, getSqmMachineId, getVSDeviceId } from 'vs/base/node/id'; import { ILogService } from 'vs/platform/log/common/log'; import { IStateReadService } from 'vs/platform/state/node/state'; -import { machineIdKey, sqmIdKey } from 'vs/platform/telemetry/common/telemetry'; +import { machineIdKey, sqmIdKey, vsDeviceIdKey } from 'vs/platform/telemetry/common/telemetry'; export async function resolveMachineId(stateService: IStateReadService, logService: ILogService): Promise { @@ -29,3 +29,12 @@ export async function resolveSqmId(stateService: IStateReadService, logService: return sqmId; } + +export async function resolveVSDeviceId(stateService: IStateReadService, logService: ILogService): Promise { + let vsDeviceId = stateService.getItem(vsDeviceIdKey); + if (typeof vsDeviceId !== 'string') { + vsDeviceId = await getVSDeviceId(logService.error.bind(logService)); + } + + return vsDeviceId; +} diff --git a/src/vs/platform/window/common/window.ts b/src/vs/platform/window/common/window.ts index 8adf80ad9c7..007c84ba19d 100644 --- a/src/vs/platform/window/common/window.ts +++ b/src/vs/platform/window/common/window.ts @@ -348,6 +348,7 @@ export interface INativeWindowConfiguration extends IWindowConfiguration, Native machineId: string; sqmId: string; + vsDeviceId: string; execPath: string; backupPath?: string; diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index 2c4a799c893..cfb6fe56efa 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -211,6 +211,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic constructor( private readonly machineId: string, private readonly sqmId: string, + private readonly vsDeviceId: string, private readonly initialUserEnv: IProcessEnvironment, @ILogService private readonly logService: ILogService, @ILoggerMainService private readonly loggerService: ILoggerMainService, @@ -1409,6 +1410,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic machineId: this.machineId, sqmId: this.sqmId, + vsDeviceId: this.vsDeviceId, windowId: -1, // Will be filled in by the window once loaded later diff --git a/src/vs/server/node/serverServices.ts b/src/vs/server/node/serverServices.ts index 07b71d03c94..b4ffa2b38c5 100644 --- a/src/vs/server/node/serverServices.ts +++ b/src/vs/server/node/serverServices.ts @@ -9,7 +9,7 @@ import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import * as path from 'vs/base/common/path'; import { IURITransformer } from 'vs/base/common/uriIpc'; -import { getMachineId, getSqmMachineId } from 'vs/base/node/id'; +import { getMachineId, getSqmMachineId, getVSDeviceId } from 'vs/base/node/id'; import { Promises } from 'vs/base/node/pfs'; import { ClientConnectionEvent, IMessagePassingProtocol, IPCServer, StaticRouter } from 'vs/base/parts/ipc/common/ipc'; import { ProtocolConstants } from 'vs/base/parts/ipc/common/ipc.net'; @@ -132,11 +132,12 @@ export async function setupServerServices(connectionToken: ServerConnectionToken socketServer.registerChannel('userDataProfiles', new RemoteUserDataProfilesServiceChannel(userDataProfilesService, (ctx: RemoteAgentConnectionContext) => getUriTransformer(ctx.remoteAuthority))); // Initialize - const [, , machineId, sqmId] = await Promise.all([ + const [, , machineId, sqmId, vsDeviceId] = await Promise.all([ configurationService.initialize(), userDataProfilesService.init(), getMachineId(logService.error.bind(logService)), - getSqmMachineId(logService.error.bind(logService)) + getSqmMachineId(logService.error.bind(logService)), + getVSDeviceId(logService.error.bind(logService)) ]); const extensionHostStatusService = new ExtensionHostStatusService(); @@ -156,7 +157,7 @@ export async function setupServerServices(connectionToken: ServerConnectionToken const config: ITelemetryServiceConfig = { appenders: [oneDsAppender], - commonProperties: resolveCommonProperties(release(), hostname(), process.arch, productService.commit, productService.version + '-remote', machineId, sqmId, isInternal, 'remoteAgent'), + commonProperties: resolveCommonProperties(release(), hostname(), process.arch, productService.commit, productService.version + '-remote', machineId, sqmId, vsDeviceId, isInternal, 'remoteAgent'), piiPaths: getPiiPathsFromEnvironment(environmentService) }; const initialTelemetryLevelArg = environmentService.args['telemetry-level']; diff --git a/src/vs/workbench/api/common/extHostTelemetry.ts b/src/vs/workbench/api/common/extHostTelemetry.ts index 896160e4d92..99d2114de97 100644 --- a/src/vs/workbench/api/common/extHostTelemetry.ts +++ b/src/vs/workbench/api/common/extHostTelemetry.ts @@ -105,6 +105,7 @@ export class ExtHostTelemetry extends Disposable implements ExtHostTelemetryShap commonProperties['common.vscodemachineid'] = this.initData.telemetryInfo.machineId; commonProperties['common.vscodesessionid'] = this.initData.telemetryInfo.sessionId; commonProperties['common.sqmid'] = this.initData.telemetryInfo.sqmId; + commonProperties['common.vsdeviceid'] = this.initData.telemetryInfo.vsDeviceId; commonProperties['common.vscodeversion'] = this.initData.version; commonProperties['common.isnewappinstall'] = isNewAppInstall(this.initData.telemetryInfo.firstSessionDate); commonProperties['common.product'] = this.initData.environment.appHost; diff --git a/src/vs/workbench/api/test/browser/extHostTelemetry.test.ts b/src/vs/workbench/api/test/browser/extHostTelemetry.test.ts index 0f79b399d9d..c66ea784c31 100644 --- a/src/vs/workbench/api/test/browser/extHostTelemetry.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTelemetry.test.ts @@ -44,7 +44,8 @@ suite('ExtHostTelemetry', function () { firstSessionDate: '2020-01-01T00:00:00.000Z', sessionId: 'test', machineId: 'test', - sqmId: 'test' + sqmId: 'test', + vsDeviceId: 'test' }; const mockRemote = { diff --git a/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts b/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts index ff7ca909d1c..f8e0c3e498a 100644 --- a/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts +++ b/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts @@ -39,6 +39,7 @@ export interface INativeWorkbenchEnvironmentService extends IBrowserWorkbenchEnv readonly os: IOSConfiguration; readonly machineId: string; readonly sqmId: string; + readonly vsDeviceId: string; // --- Paths readonly execPath: string; @@ -63,6 +64,9 @@ export class NativeWorkbenchEnvironmentService extends AbstractNativeEnvironment @memoize get sqmId() { return this.configuration.sqmId; } + @memoize + get vsDeviceId() { return this.configuration.vsDeviceId; } + @memoize get remoteAuthority() { return this.configuration.remoteAuthority; } diff --git a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts index 77969d88de9..33b5adf244a 100644 --- a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts +++ b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts @@ -310,6 +310,7 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost sessionId: this._telemetryService.sessionId, machineId: this._telemetryService.machineId, sqmId: this._telemetryService.sqmId, + vsDeviceId: this._telemetryService.vsDeviceId, firstSessionDate: this._telemetryService.firstSessionDate, msftInternal: this._telemetryService.msftInternal }, diff --git a/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts b/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts index b145bc80f6c..57cc1052139 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts @@ -41,6 +41,7 @@ export interface IExtensionHostInitData { readonly sessionId: string; readonly machineId: string; readonly sqmId: string; + readonly vsDeviceId: string; readonly firstSessionDate: string; readonly msftInternal?: boolean; }; diff --git a/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts b/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts index 5148918bbbb..331257d1e4d 100644 --- a/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts +++ b/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts @@ -244,6 +244,7 @@ export class RemoteExtensionHost extends Disposable implements IExtensionHost { sessionId: this._telemetryService.sessionId, machineId: this._telemetryService.machineId, sqmId: this._telemetryService.sqmId, + vsDeviceId: this._telemetryService.vsDeviceId, firstSessionDate: this._telemetryService.firstSessionDate, msftInternal: this._telemetryService.msftInternal }, diff --git a/src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts b/src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts index 49a9a69a671..c96ce9ba199 100644 --- a/src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts @@ -503,6 +503,7 @@ export class NativeLocalProcessExtensionHost implements IExtensionHost { sessionId: this._telemetryService.sessionId, machineId: this._telemetryService.machineId, sqmId: this._telemetryService.sqmId, + vsDeviceId: this._telemetryService.vsDeviceId, firstSessionDate: this._telemetryService.firstSessionDate, msftInternal: this._telemetryService.msftInternal }, diff --git a/src/vs/workbench/services/telemetry/browser/telemetryService.ts b/src/vs/workbench/services/telemetry/browser/telemetryService.ts index 3b7937e3afc..0feab911fda 100644 --- a/src/vs/workbench/services/telemetry/browser/telemetryService.ts +++ b/src/vs/workbench/services/telemetry/browser/telemetryService.ts @@ -29,6 +29,7 @@ export class TelemetryService extends Disposable implements ITelemetryService { get sessionId(): string { return this.impl.sessionId; } get machineId(): string { return this.impl.machineId; } get sqmId(): string { return this.impl.sqmId; } + get vsDeviceId(): string { return this.impl.vsDeviceId; } get firstSessionDate(): string { return this.impl.firstSessionDate; } get msftInternal(): boolean | undefined { return this.impl.msftInternal; } diff --git a/src/vs/workbench/services/telemetry/common/workbenchCommonProperties.ts b/src/vs/workbench/services/telemetry/common/workbenchCommonProperties.ts index 18d92118512..946adb270c8 100644 --- a/src/vs/workbench/services/telemetry/common/workbenchCommonProperties.ts +++ b/src/vs/workbench/services/telemetry/common/workbenchCommonProperties.ts @@ -17,11 +17,12 @@ export function resolveWorkbenchCommonProperties( version: string | undefined, machineId: string, sqmId: string, + vsDeviceId: string, isInternalTelemetry: boolean, process: INodeProcess, remoteAuthority?: string ): ICommonProperties { - const result = resolveCommonProperties(release, hostname, process.arch, commit, version, machineId, sqmId, isInternalTelemetry); + const result = resolveCommonProperties(release, hostname, process.arch, commit, version, machineId, sqmId, vsDeviceId, isInternalTelemetry); const firstSessionDate = storageService.get(firstSessionDateStorageKey, StorageScope.APPLICATION)!; const lastSessionDate = storageService.get(lastSessionDateStorageKey, StorageScope.APPLICATION)!; diff --git a/src/vs/workbench/services/telemetry/electron-sandbox/telemetryService.ts b/src/vs/workbench/services/telemetry/electron-sandbox/telemetryService.ts index f87e5a9f950..52f333cd51d 100644 --- a/src/vs/workbench/services/telemetry/electron-sandbox/telemetryService.ts +++ b/src/vs/workbench/services/telemetry/electron-sandbox/telemetryService.ts @@ -28,6 +28,7 @@ export class TelemetryService extends Disposable implements ITelemetryService { get sessionId(): string { return this.impl.sessionId; } get machineId(): string { return this.impl.machineId; } get sqmId(): string { return this.impl.sqmId; } + get vsDeviceId(): string { return this.impl.vsDeviceId; } get firstSessionDate(): string { return this.impl.firstSessionDate; } get msftInternal(): boolean | undefined { return this.impl.msftInternal; } @@ -45,7 +46,7 @@ export class TelemetryService extends Disposable implements ITelemetryService { const channel = sharedProcessService.getChannel('telemetryAppender'); const config: ITelemetryServiceConfig = { appenders: [new TelemetryAppenderClient(channel)], - commonProperties: resolveWorkbenchCommonProperties(storageService, environmentService.os.release, environmentService.os.hostname, productService.commit, productService.version, environmentService.machineId, environmentService.sqmId, isInternal, process, environmentService.remoteAuthority), + commonProperties: resolveWorkbenchCommonProperties(storageService, environmentService.os.release, environmentService.os.hostname, productService.commit, productService.version, environmentService.machineId, environmentService.sqmId, environmentService.vsDeviceId, isInternal, process, environmentService.remoteAuthority), piiPaths: getPiiPathsFromEnvironment(environmentService), sendErrorTelemetry: true }; diff --git a/src/vs/workbench/services/telemetry/test/node/commonProperties.test.ts b/src/vs/workbench/services/telemetry/test/node/commonProperties.test.ts index 5f47fd16202..8898cb332f7 100644 --- a/src/vs/workbench/services/telemetry/test/node/commonProperties.test.ts +++ b/src/vs/workbench/services/telemetry/test/node/commonProperties.test.ts @@ -26,7 +26,7 @@ suite('Telemetry - common properties', function () { }); test('default', function () { - const props = resolveWorkbenchCommonProperties(testStorageService, release(), hostname(), commit, version, 'someMachineId', 'someSqmId', false, process); + const props = resolveWorkbenchCommonProperties(testStorageService, release(), hostname(), commit, version, 'someMachineId', 'someSqmId', 'someVSDeviceId', false, process); assert.ok('commitHash' in props); assert.ok('sessionID' in props); assert.ok('timestamp' in props); @@ -50,14 +50,14 @@ suite('Telemetry - common properties', function () { testStorageService.store('telemetry.lastSessionDate', new Date().toUTCString(), StorageScope.APPLICATION, StorageTarget.MACHINE); - const props = resolveWorkbenchCommonProperties(testStorageService, release(), hostname(), commit, version, 'someMachineId', 'someSqmId', false, process); + const props = resolveWorkbenchCommonProperties(testStorageService, release(), hostname(), commit, version, 'someMachineId', 'someSqmId', 'someVSDeviceId', false, process); assert.ok('common.lastSessionDate' in props); // conditional, see below assert.ok('common.isNewSession' in props); assert.strictEqual(props['common.isNewSession'], '0'); }); test('values chance on ask', async function () { - const props = resolveWorkbenchCommonProperties(testStorageService, release(), hostname(), commit, version, 'someMachineId', 'someSqmId', false, process); + const props = resolveWorkbenchCommonProperties(testStorageService, release(), hostname(), commit, version, 'someMachineId', 'someSqmId', 'someVSDeviceId', false, process); let value1 = props['common.sequence']; let value2 = props['common.sequence']; assert.ok(value1 !== value2, 'seq'); diff --git a/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyBackupService.test.ts b/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyBackupService.test.ts index 4cbfe873cae..6629be8043b 100644 --- a/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyBackupService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyBackupService.test.ts @@ -56,6 +56,7 @@ const TestNativeWindowConfiguration: INativeWindowConfiguration = { windowId: 0, machineId: 'testMachineId', sqmId: 'testSqmId', + vsDeviceId: 'testVSDeviceId', logLevel: LogLevel.Error, loggers: { global: [], window: [] }, mainPid: 0, diff --git a/yarn.lock b/yarn.lock index 438959bf7de..c09fbd906ae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1517,6 +1517,14 @@ "@typescript-eslint/types" "6.21.0" eslint-visitor-keys "^3.4.1" +"@vscode/deviceid@^0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@vscode/deviceid/-/deviceid-0.1.1.tgz#750e2930a3a8fbf3fd610096a8b915dfdb493c89" + integrity sha512-ErpoMeKKNYAkR1IT3zxB5RtiTqEECdh8fxggupWvzuxpTAX77hwOI2NdJ7um+vupnXRBZVx4ugo0+dVHJWUkag== + dependencies: + fs-extra "^11.2.0" + uuid "^9.0.1" + "@vscode/gulp-electron@^1.36.0": version "1.36.0" resolved "https://registry.yarnpkg.com/@vscode/gulp-electron/-/gulp-electron-1.36.0.tgz#b2895c4bafaa0cf2b13042aa654e9fdd1f3a90cd" @@ -4754,6 +4762,15 @@ fs-constants@^1.0.0: resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== +fs-extra@^11.2.0: + version "11.2.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b" + integrity sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs-extra@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" @@ -6274,6 +6291,15 @@ jsonfile@^4.0.0: optionalDependencies: graceful-fs "^4.1.6" +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + jszip@^3.10.1: version "3.10.1" resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2" @@ -10108,6 +10134,11 @@ universalify@^0.2.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== +universalify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== + unset-value@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" @@ -10183,6 +10214,11 @@ uuid@^8.3.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +uuid@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" + integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== + v8-compile-cache-lib@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" From 2abcad8cc152e6081123e42027216cc1a9e50ea6 Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Wed, 22 May 2024 11:54:53 -0700 Subject: [PATCH 321/357] NotebookChatController still manages ChatModel --- .../inlineChat/browser/inlineChatWidget.ts | 7 +++---- .../controller/chat/notebookChatController.ts | 15 +++++++++------ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index f31b92040df..e4cfccd30bc 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -49,7 +49,6 @@ import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateF import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IHoverService } from 'vs/platform/hover/browser/hover'; import { IChatListItemRendererOptions } from 'vs/workbench/contrib/chat/browser/chat'; -import { CancellationToken } from 'vs/base/common/cancellation'; export interface InlineChatWidgetViewState { @@ -300,9 +299,9 @@ export class InlineChatWidget { // LEGACY - default chat model // this is only here for as long as we offer updateChatMessage - this._defaultChatModel = this._chatService.startSession(location, CancellationToken.None) ?? this._store.add(this._instantiationService.createInstance(ChatModel, undefined, ChatAgentLocation.Editor)); - // this._defaultChatModel.startInitialize(); - // this._defaultChatModel.initialize(undefined); + this._defaultChatModel = this._store.add(this._instantiationService.createInstance(ChatModel, undefined, ChatAgentLocation.Editor)); + this._defaultChatModel.startInitialize(); + this._defaultChatModel.initialize(undefined); this.setChatModel(this._defaultChatModel); } diff --git a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts index 2faeb9876bc..abe73bebfb2 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts @@ -5,7 +5,7 @@ import { Dimension, IFocusTracker, WindowIntervalTimer, getWindow, scheduleAtNextAnimationFrame, trackFocus } from 'vs/base/browser/dom'; import { CancelablePromise, DeferredPromise, Queue, createCancelablePromise, disposableTimeout } from 'vs/base/common/async'; -import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Emitter } from 'vs/base/common/event'; import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { LRUCache } from 'vs/base/common/map'; @@ -562,20 +562,23 @@ export class NotebookChatController extends Disposable implements INotebookEdito this._ctxHasActiveRequest.set(true); + this._activeRequestCts?.cancel(); + this._activeRequestCts = new CancellationTokenSource(); + // Start a new session if (!this._model.value) { - this._model.value = this._chatService.startSession(ChatAgentLocation.Editor, CancellationToken.None); + this._model.value = this._chatService.startSession(ChatAgentLocation.Editor, this._activeRequestCts.token); if (!this._model.value) { throw new Error('Failed to start chat session'); } } + + const model = this._model.value; + this._widget.inlineChatWidget.setChatModel(model); + this._strategy = new EditStrategy(); - - this._activeRequestCts?.cancel(); - this._activeRequestCts = new CancellationTokenSource(); - const store = new DisposableStore(); try { From b9d35d9145377a0104613c79dadeff01477dcad1 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 22 May 2024 12:07:40 -0700 Subject: [PATCH 322/357] Don't register chat participants in stable (#213244) * Don't register chat participants in stable And fork some Additions APIs into chatParticipantPrivate * Remove stale proposals * Move more API out of Additions --- extensions/vscode-api-tests/package.json | 2 - .../workbench/api/common/extHost.api.impl.ts | 4 +- .../api/common/extHostChatAgents2.ts | 30 +++- .../browser/chatParticipantContributions.ts | 5 + .../common/extensionsApiProposals.ts | 1 + ...ode.proposed.chatParticipantAdditions.d.ts | 131 ------------------ ...scode.proposed.chatParticipantPrivate.d.ts | 76 ++++++++++ .../vscode.proposed.chatVariableResolver.d.ts | 66 +++++++++ 8 files changed, 175 insertions(+), 140 deletions(-) create mode 100644 src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index 0b570051b19..51a74ad7b00 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -7,8 +7,6 @@ "enabledApiProposals": [ "activeComment", "authSession", - "chatParticipant", - "languageModels", "defaultChatParticipant", "chatVariableResolver", "contribViewsRemote", diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 5e7e148ff36..680d04cb5dd 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -209,7 +209,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostUriOpeners = rpcProtocol.set(ExtHostContext.ExtHostUriOpeners, new ExtHostUriOpeners(rpcProtocol)); const extHostProfileContentHandlers = rpcProtocol.set(ExtHostContext.ExtHostProfileContentHandlers, new ExtHostProfileContentHandlers(rpcProtocol)); rpcProtocol.set(ExtHostContext.ExtHostInteractive, new ExtHostInteractive(rpcProtocol, extHostNotebook, extHostDocumentsAndEditors, extHostCommands, extHostLogService)); - const extHostChatAgents2 = rpcProtocol.set(ExtHostContext.ExtHostChatAgents2, new ExtHostChatAgents2(rpcProtocol, extHostLogService, extHostCommands)); + const extHostChatAgents2 = rpcProtocol.set(ExtHostContext.ExtHostChatAgents2, new ExtHostChatAgents2(rpcProtocol, extHostLogService, extHostCommands, initData.quality)); const extHostChatVariables = rpcProtocol.set(ExtHostContext.ExtHostChatVariables, new ExtHostChatVariables(rpcProtocol)); const extHostAiRelatedInformation = rpcProtocol.set(ExtHostContext.ExtHostAiRelatedInformation, new ExtHostRelatedInformation(rpcProtocol)); const extHostAiEmbeddingVector = rpcProtocol.set(ExtHostContext.ExtHostAiEmbeddingVector, new ExtHostAiEmbeddingVector(rpcProtocol)); @@ -1427,7 +1427,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostChatAgents2.createChatAgent(extension, id, handler); }, createDynamicChatParticipant(id: string, dynamicProps: vscode.DynamicChatParticipantProps, handler: vscode.ChatExtendedRequestHandler): vscode.ChatParticipant { - checkProposedApiEnabled(extension, 'chatParticipantAdditions'); + checkProposedApiEnabled(extension, 'chatParticipantPrivate'); return extHostChatAgents2.createDynamicChatAgent(extension, id, dynamicProps, handler); }, attachContext(name: string, value: string | vscode.Uri | vscode.Location | unknown, location: vscode.ChatLocation.Panel) { diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 09639dd0f3a..435c4c45f69 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -263,6 +263,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS mainContext: IMainContext, private readonly _logService: ILogService, private readonly commands: ExtHostCommands, + private readonly quality: string | undefined ) { super(); this._proxy = mainContext.getProxy(MainContext.MainThreadChatAgents2); @@ -274,16 +275,19 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS createChatAgent(extension: IExtensionDescription, id: string, handler: vscode.ChatExtendedRequestHandler): vscode.ChatParticipant { const handle = ExtHostChatAgents2._idPool++; - const agent = new ExtHostChatAgent(extension, id, this._proxy, handle, handler); + const agent = new ExtHostChatAgent(extension, this.quality, id, this._proxy, handle, handler); this._agents.set(handle, agent); - this._proxy.$registerAgent(handle, extension.identifier, id, {}, undefined); + if (agent.isAgentEnabled()) { + this._proxy.$registerAgent(handle, extension.identifier, id, {}, undefined); + } + return agent.apiAgent; } createDynamicChatAgent(extension: IExtensionDescription, id: string, dynamicProps: vscode.DynamicChatParticipantProps, handler: vscode.ChatExtendedRequestHandler): vscode.ChatParticipant { const handle = ExtHostChatAgents2._idPool++; - const agent = new ExtHostChatAgent(extension, id, this._proxy, handle, handler); + const agent = new ExtHostChatAgent(extension, this.quality, id, this._proxy, handle, handler); this._agents.set(handle, agent); this._proxy.$registerAgent(handle, extension.identifier, id, { isSticky: true } satisfies IExtensionChatAgentMetadata, dynamicProps); @@ -330,6 +334,10 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS responseIsIncomplete: true }; } + if (errorDetails?.responseIsRedacted) { + checkProposedApiEnabled(agent.extension, 'chatParticipantPrivate'); + } + return { errorDetails, timings: stream.timings, metadata: result?.metadata } satisfies IChatAgentResult; }), token); } catch (e) { @@ -485,6 +493,7 @@ class ExtHostChatAgent { constructor( public readonly extension: IExtensionDescription, + private readonly quality: string | undefined, public readonly id: string, private readonly _proxy: MainThreadChatAgentsShape2, private readonly _handle: number, @@ -507,6 +516,11 @@ class ExtHostChatAgent { return await this._agentVariableProvider.provider.provideCompletionItems(query, token) ?? []; } + public isAgentEnabled() { + // If in stable and this extension doesn't have the right proposed API, then don't register the agent + return !(this.quality === 'stable' && !isProposedApiEnabled(this.extension, 'chatParticipantPrivate')); + } + async provideFollowups(result: vscode.ChatResult, context: vscode.ChatContext, token: CancellationToken): Promise { if (!this._followupProvider) { return []; @@ -564,6 +578,10 @@ class ExtHostChatAgent { } updateScheduled = true; queueMicrotask(() => { + if (!that.isAgentEnabled()) { + return; + } + this._proxy.$updateAgent(this._handle, { icon: !this._iconPath ? undefined : this._iconPath instanceof URI ? this._iconPath : @@ -658,11 +676,11 @@ class ExtHostChatAgent { updateMetadataSoon(); }, get supportIssueReporting() { - checkProposedApiEnabled(that.extension, 'chatParticipantAdditions'); + checkProposedApiEnabled(that.extension, 'chatParticipantPrivate'); return that._supportIssueReporting; }, set supportIssueReporting(v) { - checkProposedApiEnabled(that.extension, 'chatParticipantAdditions'); + checkProposedApiEnabled(that.extension, 'chatParticipantPrivate'); that._supportIssueReporting = v; updateMetadataSoon(); }, @@ -707,10 +725,12 @@ class ExtHostChatAgent { return that._requester; }, set supportsSlowReferences(v) { + checkProposedApiEnabled(that.extension, 'chatParticipantPrivate'); that._supportsSlowReferences = v; updateMetadataSoon(); }, get supportsSlowReferences() { + checkProposedApiEnabled(that.extension, 'chatParticipantPrivate'); return that._supportsSlowReferences; }, dispose() { diff --git a/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts b/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts index e9e074f0cfb..c89a6a4d712 100644 --- a/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts @@ -159,6 +159,11 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { private handleAndRegisterChatExtensions(): void { chatParticipantExtensionPoint.setHandler((extensions, delta) => { for (const extension of delta.added) { + if (this.productService.quality === 'stable' && !isProposedApiEnabled(extension.description, 'chatParticipantPrivate')) { + this.logService.warn(`Chat participants are not yet enabled in VS Code Stable (${extension.description.identifier.value})`); + continue; + } + for (const providerDescriptor of extension.value) { if (!providerDescriptor.name.match(/^[\w0-9_-]+$/)) { this.logService.error(`Extension '${extension.description.identifier.value}' CANNOT register participant with invalid name: ${providerDescriptor.name}. Name must match /^[\\w0-9_-]+$/.`); diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index 3dae0a8c70d..28cf88a67b3 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -15,6 +15,7 @@ export const allApiProposals = Object.freeze({ authSession: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authSession.d.ts', canonicalUriProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.canonicalUriProvider.d.ts', chatParticipantAdditions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts', + chatParticipantPrivate: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts', chatProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatProvider.d.ts', chatTab: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatTab.d.ts', chatVariableResolver: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts', diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index 2e89410250c..cd2ec7ba919 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -5,60 +5,8 @@ declare module 'vscode' { - /** - * The location at which the chat is happening. - */ - export enum ChatLocation { - /** - * The chat panel - */ - Panel = 1, - /** - * Terminal inline chat - */ - Terminal = 2, - /** - * Notebook inline chat - */ - Notebook = 3, - /** - * Code editor inline chat - */ - Editor = 4 - } - - export interface ChatRequest { - /** - * The attempt number of the request. The first request has attempt number 0. - */ - readonly attempt: number; - - /** - * If automatic command detection is enabled. - */ - readonly enableCommandDetection: boolean; - - /** - * The location at which the chat is happening. This will always be one of the supported values - */ - readonly location: ChatLocation; - } - export interface ChatParticipant { onDidPerformAction: Event; - supportIssueReporting?: boolean; - - /** - * Temp, support references that are slow to resolve and should be tools rather than references. - */ - supportsSlowReferences?: boolean; - } - - export interface ChatErrorDetails { - /** - * If set to true, the message content is completely hidden. Only ChatErrorDetails#message will be shown. - */ - responseIsRedacted?: boolean; } /** @@ -224,8 +172,6 @@ declare module 'vscode' { */ export function createChatParticipant(id: string, handler: ChatExtendedRequestHandler): ChatParticipant; - export function createDynamicChatParticipant(id: string, dynamicProps: DynamicChatParticipantProps, handler: ChatExtendedRequestHandler): ChatParticipant; - /** * Current version of the proposal. Changes whenever backwards-incompatible changes are made. * If a new feature is added that doesn't break existing code, the version is not incremented. When the extension uses this new feature, it should set its engines.vscode version appropriately. @@ -235,16 +181,6 @@ declare module 'vscode' { export const _version: 1 | number; } - /** - * These don't get set on the ChatParticipant after creation, like other props, because they are typically defined in package.json and we want them at the time of creation. - */ - export interface DynamicChatParticipantProps { - name: string; - publisherName: string; - description?: string; - fullName?: string; - } - /* * User action events */ @@ -313,71 +249,4 @@ declare module 'vscode' { */ readonly name: string; } - - /** - * The detail level of this chat variable value. - */ - export enum ChatVariableLevel { - Short = 1, - Medium = 2, - Full = 3 - } - - export interface ChatVariableValue { - /** - * The detail level of this chat variable value. If possible, variable resolvers should try to offer shorter values that will consume fewer tokens in an LLM prompt. - */ - level: ChatVariableLevel; - - /** - * The variable's value, which can be included in an LLM prompt as-is, or the chat participant may decide to read the value and do something else with it. - */ - value: string | Uri; - - /** - * A description of this value, which could be provided to the LLM as a hint. - */ - description?: string; - } - - export interface ChatVariableResolverResponseStream { - /** - * Push a progress part to this stream. Short-hand for - * `push(new ChatResponseProgressPart(value))`. - * - * @param value - * @returns This stream. - */ - progress(value: string): ChatVariableResolverResponseStream; - - /** - * Push a reference to this stream. Short-hand for - * `push(new ChatResponseReferencePart(value))`. - * - * *Note* that the reference is not rendered inline with the response. - * - * @param value A uri or location - * @returns This stream. - */ - reference(value: Uri | Location): ChatVariableResolverResponseStream; - - /** - * Pushes a part to this stream. - * - * @param part A response part, rendered or metadata - */ - push(part: ChatVariableResolverResponsePart): ChatVariableResolverResponseStream; - } - - export type ChatVariableResolverResponsePart = ChatResponseProgressPart | ChatResponseReferencePart; - - export interface ChatVariableResolver { - /** - * A callback to resolve the value of a chat variable. - * @param name The name of the variable. - * @param context Contextual information about this chat request. - * @param token A cancellation token. - */ - resolve2?(name: string, context: ChatVariableContext, stream: ChatVariableResolverResponseStream, token: CancellationToken): ProviderResult; - } } diff --git a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts new file mode 100644 index 00000000000..4e328978a9d --- /dev/null +++ b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts @@ -0,0 +1,76 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + /** + * The location at which the chat is happening. + */ + export enum ChatLocation { + /** + * The chat panel + */ + Panel = 1, + /** + * Terminal inline chat + */ + Terminal = 2, + /** + * Notebook inline chat + */ + Notebook = 3, + /** + * Code editor inline chat + */ + Editor = 4 + } + + export interface ChatRequest { + /** + * The attempt number of the request. The first request has attempt number 0. + */ + readonly attempt: number; + + /** + * If automatic command detection is enabled. + */ + readonly enableCommandDetection: boolean; + + /** + * The location at which the chat is happening. This will always be one of the supported values + */ + readonly location: ChatLocation; + } + + export interface ChatParticipant { + supportIssueReporting?: boolean; + + /** + * Temp, support references that are slow to resolve and should be tools rather than references. + */ + supportsSlowReferences?: boolean; + } + + export interface ChatErrorDetails { + /** + * If set to true, the message content is completely hidden. Only ChatErrorDetails#message will be shown. + */ + responseIsRedacted?: boolean; + } + + export namespace chat { + export function createDynamicChatParticipant(id: string, dynamicProps: DynamicChatParticipantProps, handler: ChatExtendedRequestHandler): ChatParticipant; + } + + /** + * These don't get set on the ChatParticipant after creation, like other props, because they are typically defined in package.json and we want them at the time of creation. + */ + export interface DynamicChatParticipantProps { + name: string; + publisherName: string; + description?: string; + fullName?: string; + } +} diff --git a/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts b/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts index 298fc82f61b..1b404980e2c 100644 --- a/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts +++ b/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts @@ -66,5 +66,71 @@ declare module 'vscode' { * @param token A cancellation token. */ resolve(name: string, context: ChatVariableContext, token: CancellationToken): ProviderResult; + + /** + * A callback to resolve the value of a chat variable. + * @param name The name of the variable. + * @param context Contextual information about this chat request. + * @param token A cancellation token. + */ + resolve2?(name: string, context: ChatVariableContext, stream: ChatVariableResolverResponseStream, token: CancellationToken): ProviderResult; } + + + /** + * The detail level of this chat variable value. + */ + export enum ChatVariableLevel { + Short = 1, + Medium = 2, + Full = 3 + } + + export interface ChatVariableValue { + /** + * The detail level of this chat variable value. If possible, variable resolvers should try to offer shorter values that will consume fewer tokens in an LLM prompt. + */ + level: ChatVariableLevel; + + /** + * The variable's value, which can be included in an LLM prompt as-is, or the chat participant may decide to read the value and do something else with it. + */ + value: string | Uri; + + /** + * A description of this value, which could be provided to the LLM as a hint. + */ + description?: string; + } + + export interface ChatVariableResolverResponseStream { + /** + * Push a progress part to this stream. Short-hand for + * `push(new ChatResponseProgressPart(value))`. + * + * @param value + * @returns This stream. + */ + progress(value: string): ChatVariableResolverResponseStream; + + /** + * Push a reference to this stream. Short-hand for + * `push(new ChatResponseReferencePart(value))`. + * + * *Note* that the reference is not rendered inline with the response. + * + * @param value A uri or location + * @returns This stream. + */ + reference(value: Uri | Location): ChatVariableResolverResponseStream; + + /** + * Pushes a part to this stream. + * + * @param part A response part, rendered or metadata + */ + push(part: ChatVariableResolverResponsePart): ChatVariableResolverResponseStream; + } + + export type ChatVariableResolverResponsePart = ChatResponseProgressPart | ChatResponseReferencePart; } From 58775f26fdaeb1081b89819934abf5614c143db1 Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Wed, 22 May 2024 12:07:40 -0700 Subject: [PATCH 323/357] Start session when notebook chat widget is created --- .../controller/chat/notebookChatController.ts | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts index abe73bebfb2..a781640e809 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts @@ -5,7 +5,7 @@ import { Dimension, IFocusTracker, WindowIntervalTimer, getWindow, scheduleAtNextAnimationFrame, trackFocus } from 'vs/base/browser/dom'; import { CancelablePromise, DeferredPromise, Queue, createCancelablePromise, disposableTimeout } from 'vs/base/common/async'; -import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { Emitter } from 'vs/base/common/event'; import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { LRUCache } from 'vs/base/common/map'; @@ -476,8 +476,9 @@ export class NotebookChatController extends Disposable implements INotebookEdito }, 0, this._store); this._sessionCtor = createCancelablePromise(async token => { - + await this._startSession(token); if (fakeParentEditor.hasModel()) { + if (this._widget) { this._focusWidget(); } @@ -494,6 +495,18 @@ export class NotebookChatController extends Disposable implements INotebookEdito }); } + private async _startSession(token: CancellationToken) { + if (!this._model.value) { + this._model.value = this._chatService.startSession(ChatAgentLocation.Editor, token); + + if (!this._model.value) { + throw new Error('Failed to start chat session'); + } + } + + this._strategy = new EditStrategy(); + } + private _scrollWidgetIntoView(index: number) { if (index === 0 || this._notebookEditor.getLength() === 0) { // the cell is at the beginning of the notebook @@ -529,6 +542,12 @@ export class NotebookChatController extends Disposable implements INotebookEdito async acceptInput() { assertType(this._widget); + await this._sessionCtor; + assertType(this._model.value); + assertType(this._strategy); + + const model = this._model.value; + this._widget.inlineChatWidget.setChatModel(model); const lastInput = this._widget.inlineChatWidget.value; this._historyUpdate(lastInput); @@ -565,20 +584,6 @@ export class NotebookChatController extends Disposable implements INotebookEdito this._activeRequestCts?.cancel(); this._activeRequestCts = new CancellationTokenSource(); - // Start a new session - - if (!this._model.value) { - this._model.value = this._chatService.startSession(ChatAgentLocation.Editor, this._activeRequestCts.token); - if (!this._model.value) { - throw new Error('Failed to start chat session'); - } - } - - - const model = this._model.value; - this._widget.inlineChatWidget.setChatModel(model); - - this._strategy = new EditStrategy(); const store = new DisposableStore(); try { From 886f3b7229708450cb15325cde2075dd0a9496fe Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 22 May 2024 21:14:57 +0200 Subject: [PATCH 324/357] Implement Profiles Editor (#213246) * first cut * second cut * fix * add more features * support skipping copying resources while creating * enable behind internal setting --- src/vs/base/browser/ui/button/button.ts | 8 +- src/vs/base/browser/ui/selectBox/selectBox.ts | 5 + .../browser/ui/selectBox/selectBoxCustom.ts | 3 + .../browser/ui/selectBox/selectBoxNative.ts | 4 + src/vs/base/browser/ui/toggle/toggle.ts | 4 +- .../userDataProfile/common/userDataProfile.ts | 1 + .../browser/extensions.contribution.ts | 4 +- .../browser/preferences.contribution.ts | 3 +- .../browser/media/userDataProfilesEditor.css | 170 +++ .../browser/userDataProfile.ts | 70 +- .../browser/userDataProfilesEditor.ts | 1025 +++++++++++++++++ .../browser/userDataProfilesEditorModel.ts | 602 ++++++++++ .../userDataProfile/common/userDataProfile.ts | 10 + .../userDataSync/browser/userDataSync.ts | 20 +- .../userDataProfileImportExportService.ts | 139 ++- .../userDataProfile/common/userDataProfile.ts | 10 +- 16 files changed, 2014 insertions(+), 64 deletions(-) create mode 100644 src/vs/workbench/contrib/userDataProfile/browser/media/userDataProfilesEditor.css create mode 100644 src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts create mode 100644 src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts create mode 100644 src/vs/workbench/contrib/userDataProfile/common/userDataProfile.ts diff --git a/src/vs/base/browser/ui/button/button.ts b/src/vs/base/browser/ui/button/button.ts index 5491cd6590e..3c42632c500 100644 --- a/src/vs/base/browser/ui/button/button.ts +++ b/src/vs/base/browser/ui/button/button.ts @@ -24,6 +24,7 @@ import 'vs/css!./button'; import { localize } from 'vs/nls'; import type { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; import { getBaseLayerHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate2'; +import { IActionProvider } from 'vs/base/browser/ui/dropdown/dropdown'; export interface IButtonOptions extends Partial { readonly title?: boolean | string; @@ -303,7 +304,7 @@ export class Button extends Disposable implements IButton { return !this._element.classList.contains('disabled'); } - private setTitle(title: string) { + setTitle(title: string) { if (!this._hover && title !== '') { this._hover = this._register(getBaseLayerHoverDelegate().setupUpdatableHover(this.options.hoverDelegate ?? getDefaultHoverDelegate('mouse'), this._element, title)); } else if (this._hover) { @@ -322,7 +323,7 @@ export class Button extends Disposable implements IButton { export interface IButtonWithDropdownOptions extends IButtonOptions { readonly contextMenuProvider: IContextMenuProvider; - readonly actions: readonly IAction[]; + readonly actions: readonly IAction[] | IActionProvider; readonly actionRunner?: IActionRunner; readonly addPrimaryActionToDropdown?: boolean; } @@ -375,9 +376,10 @@ export class ButtonWithDropdown extends Disposable implements IButton { this.dropdownButton.element.classList.add('monaco-dropdown-button'); this.dropdownButton.icon = Codicon.dropDownButton; this._register(this.dropdownButton.onDidClick(e => { + const actions = Array.isArray(options.actions) ? options.actions : (options.actions as IActionProvider).getActions(); options.contextMenuProvider.showContextMenu({ getAnchor: () => this.dropdownButton.element, - getActions: () => options.addPrimaryActionToDropdown === false ? [...options.actions] : [this.action, ...options.actions], + getActions: () => options.addPrimaryActionToDropdown === false ? [...actions] : [this.action, ...actions], actionRunner: options.actionRunner, onHide: () => this.dropdownButton.element.setAttribute('aria-expanded', 'false') }); diff --git a/src/vs/base/browser/ui/selectBox/selectBox.ts b/src/vs/base/browser/ui/selectBox/selectBox.ts index 629a39ea1d2..d5329939ca2 100644 --- a/src/vs/base/browser/ui/selectBox/selectBox.ts +++ b/src/vs/base/browser/ui/selectBox/selectBox.ts @@ -28,6 +28,7 @@ export interface ISelectBoxDelegate extends IDisposable { focus(): void; blur(): void; setFocusable(focus: boolean): void; + setEnabled(enabled: boolean): void; // Delegated Widget interface render(container: HTMLElement): void; @@ -124,6 +125,10 @@ export class SelectBox extends Widget implements ISelectBoxDelegate { this.selectBoxDelegate.setFocusable(focusable); } + setEnabled(enabled: boolean): void { + this.selectBoxDelegate.setEnabled(enabled); + } + render(container: HTMLElement): void { this.selectBoxDelegate.render(container); } diff --git a/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts b/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts index 0506315a7ae..a58782d95df 100644 --- a/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts +++ b/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts @@ -299,6 +299,9 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi } } + public setEnabled(enable: boolean): void { + this.selectElement.disabled = !enable; + } private setOptionsList() { diff --git a/src/vs/base/browser/ui/selectBox/selectBoxNative.ts b/src/vs/base/browser/ui/selectBox/selectBoxNative.ts index 12cec70bdf9..896ac0e4bd0 100644 --- a/src/vs/base/browser/ui/selectBox/selectBoxNative.ts +++ b/src/vs/base/browser/ui/selectBox/selectBoxNative.ts @@ -148,6 +148,10 @@ export class SelectBoxNative extends Disposable implements ISelectBoxDelegate { } } + public setEnabled(enable: boolean): void { + this.selectElement.disabled = !enable; + } + public setFocusable(focusable: boolean): void { this.selectElement.tabIndex = focusable ? 0 : -1; } diff --git a/src/vs/base/browser/ui/toggle/toggle.ts b/src/vs/base/browser/ui/toggle/toggle.ts index d7d01624199..a52c00287d2 100644 --- a/src/vs/base/browser/ui/toggle/toggle.ts +++ b/src/vs/base/browser/ui/toggle/toggle.ts @@ -235,6 +235,8 @@ export class Toggle extends Widget { export class Checkbox extends Widget { + static readonly CLASS_NAME = 'monaco-checkbox'; + private readonly _onChange = this._register(new Emitter()); readonly onChange: Event = this._onChange.event; @@ -246,7 +248,7 @@ export class Checkbox extends Widget { constructor(private title: string, private isChecked: boolean, styles: ICheckboxStyles) { super(); - this.checkbox = this._register(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: Checkbox.CLASS_NAME, ...unthemedToggleStyles })); this.domNode = this.checkbox.domNode; diff --git a/src/vs/platform/userDataProfile/common/userDataProfile.ts b/src/vs/platform/userDataProfile/common/userDataProfile.ts index 769f5fd2536..b65c078f83a 100644 --- a/src/vs/platform/userDataProfile/common/userDataProfile.ts +++ b/src/vs/platform/userDataProfile/common/userDataProfile.ts @@ -35,6 +35,7 @@ export const enum ProfileResourceType { * Flags to indicate whether to use the default profile or not. */ export type UseDefaultProfileFlags = { [key in ProfileResourceType]?: boolean }; +export type ProfileResourceTypeFlags = UseDefaultProfileFlags; export interface IUserDataProfile { readonly id: string; diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 57622bd5f09..92a43009c52 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -275,13 +275,13 @@ CommandsRegistry.registerCommand('_extensions.manage', (accessor: ServicesAccess } }); -CommandsRegistry.registerCommand('extension.open', async (accessor: ServicesAccessor, extensionId: string, tab?: ExtensionEditorTab, preserveFocus?: boolean, feature?: string) => { +CommandsRegistry.registerCommand('extension.open', async (accessor: ServicesAccessor, extensionId: string, tab?: ExtensionEditorTab, preserveFocus?: boolean, feature?: string, sideByside?: boolean) => { const extensionService = accessor.get(IExtensionsWorkbenchService); const commandService = accessor.get(ICommandService); const [extension] = await extensionService.getExtensions([{ id: extensionId }], CancellationToken.None); if (extension) { - return extensionService.open(extension, { tab, preserveFocus, feature }); + return extensionService.open(extension, { tab, preserveFocus, feature, sideByside }); } return commandService.executeCommand('_extensions.manage', extensionId, tab, preserveFocus, feature); diff --git a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts index 5e7f7f223d1..66ca47c40e5 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts @@ -46,6 +46,7 @@ import { SettingsEditor2Input } from 'vs/workbench/services/preferences/common/p import { IUserDataProfileService, CURRENT_PROFILE_CONTEXT } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { Categories } from 'vs/platform/action/common/actionCommonCategories'; const SETTINGS_EDITOR_COMMAND_SEARCH = 'settings.action.search'; @@ -121,7 +122,7 @@ Registry.as(EditorExtensions.EditorFactory).registerEdit const OPEN_USER_SETTINGS_UI_TITLE = nls.localize2('openSettings2', "Open Settings (UI)"); const OPEN_USER_SETTINGS_JSON_TITLE = nls.localize2('openUserSettingsJson', "Open User Settings (JSON)"); const OPEN_APPLICATION_SETTINGS_JSON_TITLE = nls.localize2('openApplicationSettingsJson', "Open Application Settings (JSON)"); -const category = nls.localize2('preferences', "Preferences"); +const category = Categories.Preferences; interface IOpenSettingsActionOptions { openToSide?: boolean; diff --git a/src/vs/workbench/contrib/userDataProfile/browser/media/userDataProfilesEditor.css b/src/vs/workbench/contrib/userDataProfile/browser/media/userDataProfilesEditor.css new file mode 100644 index 00000000000..5ed42a2fdca --- /dev/null +++ b/src/vs/workbench/contrib/userDataProfile/browser/media/userDataProfilesEditor.css @@ -0,0 +1,170 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.profiles-editor { + height: 100%; + overflow: hidden; + max-width: 1200px; + margin: 20px auto 0px auto; +} + +.profiles-editor .sidebar-view, +.profiles-editor .contents-view { + height: 100%; +} + +.profiles-editor .contents-container, +.profiles-editor .sidebar-container { + padding: 0px 20px; + height: 100%; +} + +.profiles-editor .sidebar-container .new-profile-button { + display: flex; + align-items: center; +} + +.profiles-editor .sidebar-container .new-profile-button > .monaco-button-dropdown { + flex-grow: 1; +} + +.profiles-editor .monaco-button-dropdown > .monaco-dropdown-button { + display: flex; + align-items: center; + padding: 0 4px; +} + +.profiles-editor .sidebar-container .profiles-tree { + margin-top: 10px; +} + +.profiles-editor .sidebar-container .profiles-tree .profile-tree-item { + display: flex; + align-items: center; +} + +.profiles-editor .sidebar-container .profiles-tree .profile-tree-item > * { + margin-right: 5px; +} + +.profiles-editor .sidebar-container .profiles-tree .profile-tree-item > .profile-tree-item-description { + margin-left: 2px; + display: flex; + align-items: center; + font-size: 0.9em; + opacity: 0.7; +} + +.profiles-editor .hide { + display: none !important; +} + +.profiles-editor .contents-container .profile-header { + display: flex; + height: 34px; + align-items: center; +} + +.profiles-editor .contents-container .profile-header .profile-title { + font-size: x-large; + font-weight: bold; + flex: 1; +} + +.profiles-editor .contents-container .profile-header .profile-actions-container { + display: flex; + height: 28px; +} + +.profiles-editor .contents-container .profile-header .profile-actions-container .profile-button-container { + margin-left: 5px; + min-width: 120px; +} + +.profiles-editor .contents-container .profile-header .profile-actions-container .profile-button-container .monaco-button.error, +.profiles-editor .contents-container .profile-header .profile-actions-container .profile-button-container.error { + border: 1px solid var(--vscode-inputValidation-errorBorder, transparent); +} + +.profiles-editor .contents-container .profile-header .profile-actions-container .profile-button-container .monaco-button { + padding-left: 10px; + padding-right: 10px; +} + +.profiles-editor .contents-container .profile-body { + margin-top: 20px; +} + +.profiles-editor .contents-container .profile-name-container { + margin: 0px 0px 20px 15px; + display: flex; + width: 330px; + align-items: center; +} + +.profiles-editor .contents-container .profile-name-container .codicon { + cursor: pointer; + font-size: 20px; + padding: 2px; +} + +.profiles-editor .contents-container .profile-name-container .monaco-inputbox { + flex: 1; + margin-left: 10px; +} + +.profiles-editor .contents-container .profile-select-container { + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; +} + +.profiles-editor .contents-container .profile-select-container > .monaco-select-box { + cursor: pointer; + line-height: 17px; + padding: 2px 23px 2px 8px; + border-radius: 2px; +} + +.profiles-editor .contents-container .profile-copy-from-container { + display: flex; + align-items: center; + margin: 0px 0px 20px 20px; +} + +.profiles-editor .contents-container .profile-copy-from-container > .profile-copy-from-label { + margin-right: 10px; + display: inline-flex; + align-items: center; +} + +.profiles-editor .contents-container .profile-copy-from-container > .profile-select-container { + width: 250px; +} + +.profiles-editor .contents-container .profile-contents-container { + margin: 0px 0px 10px 20px; + font-size: medium; +} + +.profiles-editor .contents-container .profile-tree-item-container { + display: flex; + align-items: center; +} + +.profiles-editor .contents-container .profile-tree-item-container.new-profile-resource-type-container > .profile-resource-type-label-container { + width: 250px; +} + +.profiles-editor .contents-container .profile-tree-item-container.new-profile-resource-type-container > .profile-select-container { + width: 170px; +} + +.profiles-editor .contents-container .profile-tree-item-container .profile-resource-type-description { + margin-left: 10px; + font-size: 0.9em; + opacity: 0.7; +} diff --git a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts index c12bb973c23..5ca000e81a7 100644 --- a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts +++ b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts @@ -23,6 +23,13 @@ import { IWorkspaceTagsService } from 'vs/workbench/contrib/tags/common/workspac import { getErrorMessage } from 'vs/base/common/errors'; import { Categories } from 'vs/platform/action/common/actionCommonCategories'; import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { EditorPaneDescriptor, IEditorPaneRegistry } from 'vs/workbench/browser/editor'; +import { EditorExtensions, IEditorFactoryRegistry } from 'vs/workbench/common/editor'; +import { UserDataProfilesEditor, UserDataProfilesEditorInput, UserDataProfilesEditorInputSerializer } from 'vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; type IProfileTemplateQuickPickItem = IQuickPickItem & IProfileTemplateInfo; @@ -62,6 +69,7 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements this.hasProfilesContext.set(this.userDataProfilesService.profiles.length > 1); this._register(this.userDataProfilesService.onDidChangeProfiles(e => this.hasProfilesContext.set(this.userDataProfilesService.profiles.length > 1))); + this.registerEditor(); this.registerActions(); if (isWeb) { @@ -71,8 +79,23 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements this.reportWorkspaceProfileInfo(); } + private registerEditor(): void { + Registry.as(EditorExtensions.EditorPane).registerEditorPane( + EditorPaneDescriptor.create( + UserDataProfilesEditor, + UserDataProfilesEditor.ID, + localize('userdataprofilesEditor', "Profiles Editor") + ), + [ + new SyncDescriptor(UserDataProfilesEditorInput) + ] + ); + Registry.as(EditorExtensions.EditorFactory).registerEditorSerializer(UserDataProfilesEditorInput.ID, UserDataProfilesEditorInputSerializer); + } + private registerActions(): void { this.registerProfileSubMenu(); + this._register(this.registerManageProfilesAction()); this._register(this.registerSwitchProfileAction()); this.registerProfilesActions(); @@ -90,7 +113,7 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements private registerProfileSubMenu(): void { const getProfilesTitle = () => { - return localize('profiles', "Profiles ({0})", this.userDataProfileService.currentProfile.name); + return localize('profiles', "Profile ({0})", this.userDataProfileService.currentProfile.name); }; MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { get title() { @@ -111,6 +134,51 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements }); } + private registerManageProfilesAction(): IDisposable { + const disposables = new DisposableStore(); + const when = ContextKeyExpr.equals('config.workbench.experimental.enableNewProfilesUI', true); + disposables.add(registerAction2(class ManageProfilesAction extends Action2 { + constructor() { + super({ + id: `workbench.profiles.actions.manageProfiles`, + title: { + ...localize2('manage profiles', "Profiles"), + mnemonicTitle: localize({ key: 'miOpenProfiles', comment: ['&& denotes a mnemonic'] }, "&&Profiles"), + }, + menu: [ + { + id: MenuId.GlobalActivity, + group: '2_configuration', + when, + order: 1 + }, + { + id: MenuId.MenubarPreferencesMenu, + group: '2_configuration', + when, + order: 1 + } + ] + }); + } + run(accessor: ServicesAccessor) { + const editorGroupsService = accessor.get(IEditorGroupsService); + const instantiationService = accessor.get(IInstantiationService); + return editorGroupsService.activeGroup.openEditor(new UserDataProfilesEditorInput(instantiationService)); + } + })); + disposables.add(MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: 'workbench.profiles.actions.manageProfiles', + category: Categories.Preferences, + title: localize2('open profiles', "Open Profiles (UI)"), + precondition: when, + }, + })); + + return disposables; + } + private readonly profilesDisposable = this._register(new MutableDisposable()); private registerProfilesActions(): void { this.profilesDisposable.value = new DisposableStore(); diff --git a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts new file mode 100644 index 00000000000..c401df78f17 --- /dev/null +++ b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts @@ -0,0 +1,1025 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./media/userDataProfilesEditor'; +import { $, addDisposableListener, append, Dimension, EventHelper, EventType, IDomPosition } from 'vs/base/browser/dom'; +import { Action, IAction, Separator, SubmenuAction } from 'vs/base/common/actions'; +import { Event } from 'vs/base/common/event'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { localize } from 'vs/nls'; +import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IUserDataProfile, IUserDataProfilesService, ProfileResourceType } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; +import { IEditorOpenContext, IEditorSerializer, IUntypedEditorInput } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { IUserDataProfilesEditor } from 'vs/workbench/contrib/userDataProfile/common/userDataProfile'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { defaultUserDataProfileIcon, IProfileResourceChildTreeItem, IProfileTemplateInfo, IUserDataProfileManagementService, PROFILE_FILTER } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { Orientation, Sizing, SplitView } from 'vs/base/browser/ui/splitview/splitview'; +import { Button, ButtonWithDropdown } from 'vs/base/browser/ui/button/button'; +import { defaultButtonStyles, defaultCheckboxStyles, defaultInputBoxStyles, defaultSelectBoxStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { registerColor } from 'vs/platform/theme/common/colorRegistry'; +import { PANEL_BORDER } from 'vs/workbench/common/theme'; +import { WorkbenchAsyncDataTree, WorkbenchObjectTree } from 'vs/platform/list/browser/listService'; +import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { IAsyncDataSource, IObjectTreeElement, ITreeNode, ITreeRenderer, ObjectTreeElementCollapseState } from 'vs/base/browser/ui/tree/tree'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; +import { Disposable, DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; +import { Checkbox } from 'vs/base/browser/ui/toggle/toggle'; +import { DEFAULT_ICON, ICONS } from 'vs/workbench/services/userDataProfile/common/userDataProfileIcons'; +import { WorkbenchIconSelectBox } from 'vs/workbench/services/userDataProfile/browser/iconSelectBox'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { IHoverService, WorkbenchHoverDelegate } from 'vs/platform/hover/browser/hover'; +import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; +import { IHoverWidget } from 'vs/base/browser/ui/hover/hover'; +import { ISelectOptionItem, SelectBox } from 'vs/base/browser/ui/selectBox/selectBox'; +import { URI } from 'vs/base/common/uri'; +import { IEditorProgressService } from 'vs/platform/progress/common/progress'; +import { ExtensionsResourceTreeItem } from 'vs/workbench/services/userDataProfile/browser/extensionsResource'; +import { isString, isUndefined } from 'vs/base/common/types'; +import { basename } from 'vs/base/common/resources'; +import { RenderIndentGuides } from 'vs/base/browser/ui/tree/abstractTree'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { API_OPEN_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; +import { SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; +import { DEFAULT_LABELS_CONTAINER, IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; +import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { AbstractUserDataProfileElement, IProfileElement, NewProfileElement, UserDataProfileElement, UserDataProfilesEditorModel } from 'vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel'; +import { Codicon } from 'vs/base/common/codicons'; + +export const profilesSashBorder = registerColor('profiles.sashBorder', { dark: PANEL_BORDER, light: PANEL_BORDER, hcDark: PANEL_BORDER, hcLight: PANEL_BORDER }, localize('profilesSashBorder', "The color of the Profiles editor splitview sash border.")); + +export class UserDataProfilesEditor extends EditorPane implements IUserDataProfilesEditor { + + static readonly ID: string = 'workbench.editor.userDataProfiles'; + + private container: HTMLElement | undefined; + private splitView: SplitView | undefined; + private profilesTree: WorkbenchObjectTree | undefined; + private profileWidget: ProfileWidget | undefined; + + private model: UserDataProfilesEditorModel | undefined; + private templates: IProfileTemplateInfo[] = []; + + constructor( + group: IEditorGroup, + @ITelemetryService telemetryService: ITelemetryService, + @IThemeService themeService: IThemeService, + @IStorageService storageService: IStorageService, + @IUserDataProfileManagementService private readonly userDataProfileManagementService: IUserDataProfileManagementService, + @IQuickInputService private readonly quickInputService: IQuickInputService, + @IFileDialogService private readonly fileDialogService: IFileDialogService, + @IContextMenuService private readonly contextMenuService: IContextMenuService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(UserDataProfilesEditor.ID, group, telemetryService, themeService, storageService); + } + + layout(dimension: Dimension, position?: IDomPosition | undefined): void { + if (this.container && this.splitView) { + const height = dimension.height - 20; + this.splitView.layout(this.container?.clientWidth, height); + this.splitView.el.style.height = `${height}px`; + } + } + + protected createEditor(parent: HTMLElement): void { + this.container = append(parent, $('.profiles-editor')); + + const sidebarView = append(this.container, $('.sidebar-view')); + const sidebarContainer = append(sidebarView, $('.sidebar-container')); + + const contentsView = append(this.container, $('.contents-view')); + const contentsContainer = append(contentsView, $('.contents-container')); + this.profileWidget = this._register(this.instantiationService.createInstance(ProfileWidget, contentsContainer)); + + this.splitView = new SplitView(this.container, { + orientation: Orientation.HORIZONTAL, + proportionalLayout: true + }); + + this.renderSidebar(sidebarContainer); + this.splitView.addView({ + onDidChange: Event.None, + element: sidebarView, + minimumSize: 175, + maximumSize: 300, + layout: (width, _, height) => { + sidebarView.style.width = `${width}px`; + if (height && this.profilesTree) { + this.profilesTree.getHTMLElement().style.height = `${height - 38}px`; + this.profilesTree.layout(height - 38, width); + } + } + }, 250, undefined, true); + this.splitView.addView({ + onDidChange: Event.None, + element: contentsView, + minimumSize: 500, + maximumSize: Number.POSITIVE_INFINITY, + layout: (width, _, height) => { + contentsView.style.width = `${width}px`; + if (height) { + this.profileWidget?.layout(new Dimension(width, height)); + } + } + }, Sizing.Distribute, undefined, true); + + const borderColor = this.theme.getColor(profilesSashBorder)!; + this.splitView.style({ separatorBorder: borderColor }); + + this.registerListeners(); + + this.userDataProfileManagementService.getBuiltinProfileTemplates().then(templates => { + this.templates = templates; + this.profileWidget!.templates = templates; + }); + } + + private renderSidebar(parent: HTMLElement): void { + // render New Profile Button + this.renderNewProfileButton(append(parent, $('.new-profile-button'))); + + // render profiles and templates tree + const renderer = this.instantiationService.createInstance(ProfileTreeElementRenderer); + const delegate = new ProfileTreeElementDelegate(); + this.profilesTree = this._register(this.instantiationService.createInstance(WorkbenchObjectTree, 'ProfilesTree', + append(parent, $('.profiles-tree')), + delegate, + [renderer], + { + multipleSelectionSupport: false, + setRowLineHeight: false, + horizontalScrolling: false, + accessibilityProvider: { + getAriaLabel(extensionFeature: IProfileElement | null): string { + return extensionFeature?.name ?? ''; + }, + getWidgetAriaLabel(): string { + return localize('profiles', "Profiles"); + } + }, + openOnSingleClick: true, + enableStickyScroll: false, + identityProvider: { + getId(e) { + if (e instanceof UserDataProfileElement) { + return e.profile.id; + } + return e.name; + } + } + })); + } + + private renderNewProfileButton(parent: HTMLElement): void { + const button = this._register(new ButtonWithDropdown(parent, { + actions: { + getActions: () => { + const actions: IAction[] = []; + if (this.templates.length) { + actions.push(new SubmenuAction('from.template', localize('from template', "From Template"), + this.templates.map(template => new Action(`template:${template.url}`, template.name, undefined, true, async () => { + this.model?.createNewProfile(URI.parse(template.url)); + })))); + actions.push(new Separator()); + } + actions.push(new Action('importProfile', localize('importProfile', "Import Profile..."), undefined, true, () => this.importProfile())); + return actions; + } + }, + addPrimaryActionToDropdown: false, + contextMenuProvider: this.contextMenuService, + supportIcons: true, + ...defaultButtonStyles + })); + button.label = `$(add) ${localize('newProfile', "New Profile")}`; + this._register(button.onDidClick(e => { + if (this.model) { + const element = this.model.createNewProfile(); + this.updateProfilesTree(element); + } + })); + } + + private registerListeners(): void { + if (this.profilesTree) { + this._register(this.profilesTree.onDidChangeSelection(e => { + const [element] = e.elements; + if (element instanceof AbstractUserDataProfileElement) { + this.profileWidget?.render(element); + } + })); + + this._register(this.profilesTree.onContextMenu(e => { + if (e.element instanceof AbstractUserDataProfileElement) { + this.contextMenuService.showContextMenu({ + getAnchor: () => e.anchor, + getActions: () => e.element instanceof AbstractUserDataProfileElement ? e.element.secondaryActions : [], + getActionsContext: () => e.element + }); + } + })); + } + } + + private async importProfile(): Promise { + const disposables = new DisposableStore(); + const quickPick = disposables.add(this.quickInputService.createQuickPick()); + + const updateQuickPickItems = (value?: string) => { + const quickPickItems: IQuickPickItem[] = []; + if (value) { + quickPickItems.push({ label: quickPick.value, description: localize('import from url', "Import from URL") }); + } + quickPickItems.push({ label: localize('import from file', "Select File...") }); + quickPick.items = quickPickItems; + }; + + quickPick.title = localize('import profile quick pick title', "Import from Profile Template..."); + quickPick.placeholder = localize('import profile placeholder', "Provide Profile Template URL"); + quickPick.ignoreFocusOut = true; + disposables.add(quickPick.onDidChangeValue(updateQuickPickItems)); + updateQuickPickItems(); + quickPick.matchOnLabel = false; + quickPick.matchOnDescription = false; + disposables.add(quickPick.onDidAccept(async () => { + quickPick.hide(); + const selectedItem = quickPick.selectedItems[0]; + if (!selectedItem) { + return; + } + const url = selectedItem.label === quickPick.value ? URI.parse(quickPick.value) : await this.getProfileUriFromFileSystem(); + if (url) { + this.model?.createNewProfile(url); + } + })); + disposables.add(quickPick.onDidHide(() => disposables.dispose())); + quickPick.show(); + } + + + private async getProfileUriFromFileSystem(): Promise { + const profileLocation = await this.fileDialogService.showOpenDialog({ + canSelectFolders: false, + canSelectFiles: true, + canSelectMany: false, + filters: PROFILE_FILTER, + title: localize('import profile dialog', "Select Profile Template File"), + }); + if (!profileLocation) { + return null; + } + return profileLocation[0]; + } + + override async setInput(input: UserDataProfilesEditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + await super.setInput(input, options, context, token); + this.model = await input.resolve(); + this.updateProfilesTree(); + this._register(this.model.onDidChange((element) => { + this.updateProfilesTree(element); + })); + } + + override focus(): void { + super.focus(); + this.profilesTree?.domFocus(); + } + + private updateProfilesTree(elementToSelect?: IProfileElement): void { + if (!this.model) { + return; + } + const profileElements: IObjectTreeElement[] = this.model.profiles.map(element => ({ element })); + const currentSelection = this.profilesTree?.getSelection()?.[0]; + this.profilesTree?.setChildren(null, [ + { + element: { name: localize('profiles', "Profiles") }, + children: profileElements, + collapsible: false, + collapsed: ObjectTreeElementCollapseState.Expanded + } + ]); + if (elementToSelect) { + this.profilesTree?.setSelection([elementToSelect]); + } else if (currentSelection) { + if (currentSelection instanceof AbstractUserDataProfileElement) { + if (!this.model.profiles.includes(currentSelection)) { + const elementToSelect = this.model.profiles.find(profile => profile.name === currentSelection.name) ?? this.model.profiles[0]; + if (elementToSelect) { + this.profilesTree?.setSelection([elementToSelect]); + } + } + } + } else { + const elementToSelect = this.model.profiles.find(profile => profile.active) ?? this.model.profiles[0]; + if (elementToSelect) { + this.profilesTree?.setSelection([elementToSelect]); + } + } + } + +} + +interface IProfileTreeElementTemplateData { + readonly icon: HTMLElement; + readonly label: HTMLElement; + readonly description: HTMLElement; + readonly disposables: DisposableStore; +} + +class ProfileTreeElementDelegate implements IListVirtualDelegate { + getHeight(element: IProfileElement) { + return 30; + } + getTemplateId() { return 'profileTreeElement'; } +} + +class ProfileTreeElementRenderer implements ITreeRenderer { + + readonly templateId = 'profileTreeElement'; + + renderTemplate(container: HTMLElement): IProfileTreeElementTemplateData { + container.classList.add('profile-tree-item'); + const icon = append(container, $('.profile-tree-item-icon')); + const label = append(container, $('.profile-tree-item-label')); + const description = append(container, $('.profile-tree-item-description')); + append(description, $(`span${ThemeIcon.asCSSSelector(Codicon.check)}`)); + append(description, $('span', undefined, localize('activeProfile', "Active"))); + return { label, icon, description, disposables: new DisposableStore() }; + } + + renderElement({ element }: ITreeNode, index: number, templateData: IProfileTreeElementTemplateData, height: number | undefined): void { + templateData.disposables.clear(); + templateData.label.textContent = element.name; + if (element.icon) { + templateData.icon.className = ThemeIcon.asClassName(ThemeIcon.fromId(element.icon)); + } else { + templateData.icon.className = 'hide'; + } + templateData.description.classList.toggle('hide', !element.active); + if (element.onDidChange) { + templateData.disposables.add(element.onDidChange(e => { + if (e.name) { + templateData.label.textContent = element.name; + } + if (e.icon) { + if (element.icon) { + templateData.icon.className = ThemeIcon.asClassName(ThemeIcon.fromId(element.icon)); + } else { + templateData.icon.className = 'hide'; + } + } + if (e.active) { + templateData.description.classList.toggle('hide', !element.active); + } + })); + } + } + + disposeTemplate(templateData: IProfileTreeElementTemplateData): void { + templateData.disposables.dispose(); + } +} + +class ProfileWidget extends Disposable { + + private readonly profileTitle: HTMLElement; + private readonly actionbar: ActionBar; + private readonly buttonContainer: HTMLElement; + private readonly iconElement: HTMLElement; + private readonly nameContainer: HTMLElement; + private readonly nameInput: InputBox; + private readonly copyFromContainer: HTMLElement; + private readonly copyFromSelectBox: SelectBox; + private copyFromOptions: (ISelectOptionItem & { id?: string; source?: IUserDataProfile | URI })[] = []; + + private readonly resourcesTree: WorkbenchAsyncDataTree; + + private _templates: IProfileTemplateInfo[] = []; + public set templates(templates: IProfileTemplateInfo[]) { + this._templates = templates; + this.renderSelectBox(); + } + + private readonly _profileElement = this._register(new MutableDisposable<{ element: AbstractUserDataProfileElement } & IDisposable>()); + + constructor( + parent: HTMLElement, + @IHoverService private readonly hoverService: IHoverService, + @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, + @IContextViewService private readonly contextViewService: IContextViewService, + @IEditorProgressService private readonly editorProgressService: IEditorProgressService, + @ICommandService private readonly commandService: ICommandService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(); + + const header = append(parent, $('.profile-header')); + const title = append(header, $('.profile-title')); + append(title, $('span', undefined, localize('profile', "Profile: "))); + this.profileTitle = append(title, $('span')); + const actionsContainer = append(header, $('.profile-actions-container')); + this.actionbar = new ActionBar(actionsContainer, { + focusOnlyEnabledItems: true + }); + this.actionbar.setFocusable(false); + this.buttonContainer = append(actionsContainer, $('.profile-button-container')); + + const body = append(parent, $('.profile-body')); + + this.nameContainer = append(body, $('.profile-name-container')); + this.iconElement = append(this.nameContainer, $(`${ThemeIcon.asCSSSelector(DEFAULT_ICON)}`, { 'tabindex': '0', 'role': 'button', 'aria-label': localize('icon', "Profile Icon") })); + this.renderIconSelectBox(this.iconElement); + + this.nameInput = this._register(new InputBox( + this.nameContainer, + undefined, + { + inputBoxStyles: defaultInputBoxStyles, + ariaLabel: localize('profileName', "Profile Name"), + placeholder: localize('profileName', "Profile Name"), + } + )); + this.nameInput.onDidChange(value => { + if (this._profileElement.value) { + this._profileElement.value.element.name = value; + } + }); + + this.copyFromContainer = append(body, $('.profile-copy-from-container')); + append(this.copyFromContainer, $('.profile-copy-from-label', undefined, localize('create from', "Copy from:"))); + this.copyFromSelectBox = this._register(this.instantiationService.createInstance(SelectBox, + [], + 0, + this.contextViewService, + defaultSelectBoxStyles, + { + useCustomDrawn: true, + ariaLabel: localize('copy profile from', "Copy profile from"), + } + )); + this.copyFromSelectBox.render(append(this.copyFromContainer, $('.profile-select-container'))); + + const contentsContainer = append(body, $('.profile-contents-container')); + append(contentsContainer, $('.profile-contents-label', undefined, localize('contents', "Contents"))); + + const delegate = new ProfileResourceTreeElementDelegate(); + this.resourcesTree = this._register(this.instantiationService.createInstance(WorkbenchAsyncDataTree, + 'ProfileEditor-ResourcesTree', + append(body, $('.profile-content-tree.file-icon-themable-tree.show-file-icons')), + delegate, + [ + this.instantiationService.createInstance(ExistingProfileResourceTreeRenderer), + this.instantiationService.createInstance(NewProfileResourceTreeRenderer), + this.instantiationService.createInstance(ProfileResourceChildTreeItemRenderer), + ], + this.instantiationService.createInstance(ProfileResourceTreeDataSource), + { + multipleSelectionSupport: false, + horizontalScrolling: false, + accessibilityProvider: { + getAriaLabel(element: ProfileResourceTreeElement | null): string { + if (isString(element?.element)) { + return element.element; + } + if (element?.element) { + return element.element.label?.label ?? ''; + } + return ''; + }, + getWidgetAriaLabel(): string { + return ''; + }, + }, + identityProvider: { + getId(element) { + if (isString(element?.element)) { + return element.element; + } + if (element?.element) { + return element.element.handle; + } + return ''; + } + }, + expandOnlyOnTwistieClick: true, + renderIndentGuides: RenderIndentGuides.None, + openOnSingleClick: true, + enableStickyScroll: false, + })); + this._register(this.resourcesTree.onDidOpen(async (e) => { + if (!e.browserEvent) { + return; + } + if (e.browserEvent.target && (e.browserEvent.target as HTMLElement).classList.contains(Checkbox.CLASS_NAME)) { + return; + } + if (e.element && !isString(e.element.element)) { + if (e.element.element.resourceUri) { + await this.commandService.executeCommand(API_OPEN_EDITOR_COMMAND_ID, e.element.element.resourceUri, [SIDE_GROUP], undefined, e); + } else if (e.element.element.parent instanceof ExtensionsResourceTreeItem) { + await this.commandService.executeCommand('extension.open', e.element.element.handle, undefined, true, undefined, true); + } + } + })); + } + + private renderIconSelectBox(iconContainer: HTMLElement): void { + const iconSelectBox = this._register(this.instantiationService.createInstance(WorkbenchIconSelectBox, { icons: ICONS, inputBoxStyles: defaultInputBoxStyles })); + let hoverWidget: IHoverWidget | undefined; + const showIconSelectBox = () => { + if (this._profileElement.value?.element instanceof UserDataProfileElement && this._profileElement.value.element.profile.isDefault) { + return; + } + iconSelectBox.clearInput(); + hoverWidget = this.hoverService.showHover({ + content: iconSelectBox.domNode, + target: iconContainer, + position: { + hoverPosition: HoverPosition.BELOW, + }, + persistence: { + sticky: true, + }, + appearance: { + showPointer: true, + }, + }, true); + + if (hoverWidget) { + iconSelectBox.layout(new Dimension(486, 260)); + iconSelectBox.focus(); + } + }; + this._register(addDisposableListener(iconContainer, EventType.CLICK, (e: MouseEvent) => { + EventHelper.stop(e, true); + showIconSelectBox(); + })); + this._register(addDisposableListener(iconContainer, EventType.KEY_DOWN, e => { + const event = new StandardKeyboardEvent(e); + if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) { + EventHelper.stop(event, true); + showIconSelectBox(); + } + })); + this._register(addDisposableListener(iconSelectBox.domNode, EventType.KEY_DOWN, e => { + const event = new StandardKeyboardEvent(e); + if (event.equals(KeyCode.Escape)) { + EventHelper.stop(event, true); + hoverWidget?.dispose(); + iconContainer.focus(); + } + })); + this._register(iconSelectBox.onDidSelect(selectedIcon => { + hoverWidget?.dispose(); + iconContainer.focus(); + if (this._profileElement.value) { + this._profileElement.value.element.icon = selectedIcon.id; + } + })); + } + + private renderSelectBox(): void { + const separator = { text: '\u2500\u2500\u2500\u2500\u2500\u2500', isDisabled: true }; + this.copyFromOptions.push({ text: localize('empty profile', "None") }); + if (this._templates.length) { + this.copyFromOptions.push({ ...separator, decoratorRight: localize('from templates', "Profile Templates") }); + for (const template of this._templates) { + this.copyFromOptions.push({ text: template.name, id: template.url, source: URI.parse(template.url) }); + } + } + this.copyFromOptions.push({ ...separator, decoratorRight: localize('from existing profiles', "Existing Profiles") }); + for (const profile of this.userDataProfilesService.profiles) { + this.copyFromOptions.push({ text: profile.name, id: profile.id, source: profile }); + } + this.copyFromSelectBox.setOptions(this.copyFromOptions); + this._register(this.copyFromSelectBox.onDidSelect(option => { + if (this._profileElement.value?.element instanceof NewProfileElement) { + this._profileElement.value.element.copyFrom = this.copyFromOptions[option.index].source; + } + })); + } + + layout(dimension: Dimension): void { + this.resourcesTree.layout(dimension.height - 34 - 20 - 25 - 20, dimension.width); + } + + render(profileElement: AbstractUserDataProfileElement): void { + const disposables = new DisposableStore(); + this._profileElement.value = { element: profileElement, dispose: () => disposables.dispose() }; + + this.renderProfileElement(profileElement); + disposables.add(profileElement.onDidChange(e => this.renderProfileElement(profileElement))); + + const profile = profileElement instanceof UserDataProfileElement ? profileElement.profile : undefined; + this.nameInput.setEnabled(!profile?.isDefault); + + this.resourcesTree.setInput(profileElement); + disposables.add(profileElement.onDidChange(e => { + if (e.flags || e.copyFrom) { + const viewState = this.resourcesTree.getViewState(); + this.resourcesTree.setInput(profileElement, { + ...viewState, + expanded: viewState.expanded?.map(e => e) + }); + } + })); + + const button = disposables.add(new Button(this.buttonContainer, { + supportIcons: true, + ...defaultButtonStyles + })); + button.label = profileElement.primaryAction.label; + button.enabled = profileElement.primaryAction.enabled; + disposables.add(button.onDidClick(() => this.editorProgressService.showWhile(profileElement.primaryAction.run()))); + disposables.add(profileElement.primaryAction.onDidChange((e) => { + if (!isUndefined(e.enabled)) { + button.enabled = profileElement.primaryAction.enabled; + } + })); + disposables.add(profileElement.onDidChange(e => { + if (e.message) { + button.setTitle(profileElement.message ?? profileElement.primaryAction.label); + button.element.classList.toggle('error', !!profileElement.message); + } + })); + + this.actionbar.clear(); + if (profileElement.secondaryActions.length > 0) { + this.actionbar.push(profileElement.secondaryActions.filter(a => !(a instanceof Separator)), { icon: true, label: false }); + } + + this.nameInput.focus(); + if (profileElement instanceof NewProfileElement) { + this.nameInput.select(); + } + } + + private renderProfileElement(profileElement: AbstractUserDataProfileElement): void { + this.profileTitle.textContent = profileElement.name; + this.nameInput.value = profileElement.name; + if (profileElement.icon) { + this.iconElement.className = ThemeIcon.asClassName(ThemeIcon.fromId(profileElement.icon)); + } else { + this.iconElement.className = ThemeIcon.asClassName(ThemeIcon.fromId(DEFAULT_ICON.id)); + } + if (profileElement instanceof NewProfileElement) { + this.copyFromContainer.classList.remove('hide'); + const id = profileElement.copyFrom instanceof URI ? profileElement.copyFrom.toString() : profileElement.copyFrom?.id; + const index = id + ? this.copyFromOptions.findIndex(option => option.id === id) + : 0; + if (index !== -1) { + this.copyFromSelectBox.setOptions(this.copyFromOptions); + this.copyFromSelectBox.setEnabled(true); + this.copyFromSelectBox.select(index); + } else { + this.copyFromSelectBox.setOptions([{ text: basename(profileElement.copyFrom as URI) }]); + this.copyFromSelectBox.setEnabled(false); + } + } else { + this.copyFromContainer.classList.add('hide'); + } + } +} + + +interface ProfileResourceTreeElement { + element: ProfileResourceType | IProfileResourceChildTreeItem; + root: AbstractUserDataProfileElement; +} + +class ProfileResourceTreeElementDelegate implements IListVirtualDelegate { + getTemplateId(element: ProfileResourceTreeElement) { + if (!isString(element.element)) { + return ProfileResourceChildTreeItemRenderer.TEMPLATE_ID; + } + if (element.root instanceof NewProfileElement) { + return NewProfileResourceTreeRenderer.TEMPLATE_ID; + } + return ExistingProfileResourceTreeRenderer.TEMPLATE_ID; + } + getHeight(element: ProfileResourceTreeElement) { + return 30; + } +} + +class ProfileResourceTreeDataSource implements IAsyncDataSource { + + constructor( + @IEditorProgressService private readonly editorProgressService: IEditorProgressService, + ) { } + + hasChildren(element: AbstractUserDataProfileElement | ProfileResourceTreeElement): boolean { + if (element instanceof AbstractUserDataProfileElement) { + return true; + } + if (isString(element.element)) { + if (element.root.getFlag(element.element)) { + return false; + } + if (element.root instanceof NewProfileElement) { + return element.root.copyFrom !== undefined; + } + return true; + } + return false; + } + + async getChildren(element: AbstractUserDataProfileElement | ProfileResourceTreeElement): Promise { + if (element instanceof AbstractUserDataProfileElement) { + const resourceTypes = [ + ProfileResourceType.Settings, + ProfileResourceType.Keybindings, + ProfileResourceType.Snippets, + ProfileResourceType.Tasks, + ProfileResourceType.Extensions + ]; + return resourceTypes.map(resourceType => ({ element: resourceType, root: element })); + } + if (isString(element.element)) { + const progressRunner = this.editorProgressService.show(true); + try { + const extensions = await element.root.getChildren(element.element); + return extensions.map(extension => ({ element: extension, root: element.root })); + } finally { + progressRunner.done(); + } + } + return []; + } +} + +interface IProfileResourceTemplateData { + readonly disposables: DisposableStore; + readonly elementDisposables: DisposableStore; +} + +interface IExistingProfileResourceTemplateData extends IProfileResourceTemplateData { + readonly checkbox: Checkbox; + readonly label: HTMLElement; + readonly description: HTMLElement; +} + +interface INewProfileResourceTemplateData extends IProfileResourceTemplateData { + readonly label: HTMLElement; + readonly selectContainer: HTMLElement; + readonly selectBox: SelectBox; + readonly description: HTMLElement; +} + +interface IProfileResourceChildTreeItemTemplateData extends IProfileResourceTemplateData { + readonly checkbox: Checkbox; + readonly resourceLabel: IResourceLabel; +} + +class AbstractProfileResourceTreeRenderer extends Disposable { + + protected getResourceTypeTitle(resourceType: ProfileResourceType): string { + switch (resourceType) { + case ProfileResourceType.Settings: + return localize('settings', "Settings"); + case ProfileResourceType.Keybindings: + return localize('keybindings', "Keyboard Shortcuts"); + case ProfileResourceType.Snippets: + return localize('snippets', "User Snippets"); + case ProfileResourceType.Tasks: + return localize('tasks', "User Tasks"); + case ProfileResourceType.Extensions: + return localize('extensions', "Extensions"); + } + return ''; + } + + disposeElement(element: ITreeNode, index: number, templateData: IProfileResourceTemplateData, height: number | undefined): void { + templateData.elementDisposables.clear(); + } + + disposeTemplate(templateData: IProfileResourceTemplateData): void { + templateData.disposables.dispose(); + } +} + + +class ExistingProfileResourceTreeRenderer extends AbstractProfileResourceTreeRenderer implements ITreeRenderer { + + static readonly TEMPLATE_ID = 'ExistingProfileResourceTemplate'; + + readonly templateId = ExistingProfileResourceTreeRenderer.TEMPLATE_ID; + + renderTemplate(parent: HTMLElement): IExistingProfileResourceTemplateData { + const disposables = new DisposableStore(); + const container = append(parent, $('.profile-tree-item-container.existing-profile-resource-type-container')); + const checkbox = disposables.add(new Checkbox('', false, defaultCheckboxStyles)); + append(container, checkbox.domNode); + const label = append(container, $('.profile-resource-type-label')); + const description = append(container, $('.profile-resource-type-description', undefined, localize('using defaults', "Using Default Profile"))); + return { checkbox, label, description, disposables, elementDisposables: disposables.add(new DisposableStore()) }; + } + + renderElement({ element: profileResourceTreeElement }: ITreeNode, index: number, templateData: IExistingProfileResourceTemplateData, height: number | undefined): void { + templateData.elementDisposables.clear(); + const { element, root } = profileResourceTreeElement; + if (!(root instanceof UserDataProfileElement)) { + throw new Error('ExistingProfileResourceTreeRenderer can only render existing profile element'); + } + if (!isString(element)) { + throw new Error('ExistingProfileResourceTreeRenderer can only render profile resource types'); + } + + templateData.label.textContent = this.getResourceTypeTitle(element); + if (root instanceof UserDataProfileElement && root.profile.isDefault) { + templateData.checkbox.checked = true; + templateData.checkbox.disable(); + templateData.description.classList.add('hide'); + } else { + templateData.checkbox.enable(); + const checked = !root.getFlag(element); + templateData.checkbox.checked = checked; + templateData.description.classList.toggle('hide', checked); + templateData.elementDisposables.add(templateData.checkbox.onChange(() => root.setFlag(element, !templateData.checkbox.checked))); + templateData.elementDisposables.add(root.onDidChange(e => { + if (e.flags) { + templateData.description.classList.toggle('hide', !root.getFlag(element)); + } + })); + } + } + +} + +class NewProfileResourceTreeRenderer extends AbstractProfileResourceTreeRenderer implements ITreeRenderer { + + static readonly TEMPLATE_ID = 'NewProfileResourceTemplate'; + + readonly templateId = NewProfileResourceTreeRenderer.TEMPLATE_ID; + + constructor( + @IContextViewService private readonly contextViewService: IContextViewService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(); + } + + renderTemplate(parent: HTMLElement): INewProfileResourceTemplateData { + const disposables = new DisposableStore(); + const container = append(parent, $('.profile-tree-item-container.new-profile-resource-type-container')); + const labelContainer = append(container, $('.profile-resource-type-label-container')); + const label = append(labelContainer, $('span.profile-resource-type-label')); + const description = append(labelContainer, $('span.profile-resource-type-description', undefined, localize('use default', "Use Default Profile"))); + const selectBox = this._register(this.instantiationService.createInstance(SelectBox, + [ + { text: localize('copy', "Copy"), description: localize('copy from selected', "Copy from selected") }, + { text: localize('empty', "Empty") }, + { text: localize('exclude', "Exclude"), description: localize('use default', "Use Default Profile") } + ], + 0, + this.contextViewService, + defaultSelectBoxStyles, + { + useCustomDrawn: true, + } + )); + const selectContainer = append(container, $('.profile-select-container')); + selectBox.render(selectContainer); + + return { label, selectContainer, selectBox, description, disposables, elementDisposables: disposables.add(new DisposableStore()) }; + } + + renderElement({ element: profileResourceTreeElement }: ITreeNode, index: number, templateData: INewProfileResourceTemplateData, height: number | undefined): void { + templateData.elementDisposables.clear(); + const { element, root } = profileResourceTreeElement; + if (!(root instanceof NewProfileElement)) { + throw new Error('NewProfileResourceTreeRenderer can only render new profile element'); + } + if (!isString(element)) { + throw new Error('NewProfileResourceTreeRenderer can only profile resoyrce types'); + } + templateData.label.textContent = this.getResourceTypeTitle(element); + templateData.description.classList.toggle('hide', !root.getFlag(element)); + templateData.selectBox.select(root.getCopyFlag(element) ? 0 : root.getFlag(element) ? 2 : 1); + templateData.elementDisposables.add(templateData.selectBox.onDidSelect(option => { + root.setFlag(element, option.index === 2); + root.setCopyFlag(element, option.index === 0); + })); + templateData.elementDisposables.add(root.onDidChange(e => { + if (e.flags) { + templateData.description.classList.toggle('hide', !root.getFlag(element)); + } + })); + } +} + +class ProfileResourceChildTreeItemRenderer extends AbstractProfileResourceTreeRenderer implements ITreeRenderer { + + static readonly TEMPLATE_ID = 'ProfileResourceChildTreeItemTemplate'; + + readonly templateId = ProfileResourceChildTreeItemRenderer.TEMPLATE_ID; + private readonly labels: ResourceLabels; + private readonly hoverDelegate: IHoverDelegate; + + constructor( + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(); + this.labels = instantiationService.createInstance(ResourceLabels, DEFAULT_LABELS_CONTAINER); + this.hoverDelegate = this._register(instantiationService.createInstance(WorkbenchHoverDelegate, 'mouse', false, {})); + } + + renderTemplate(parent: HTMLElement): IProfileResourceChildTreeItemTemplateData { + const disposables = new DisposableStore(); + const container = append(parent, $('.profile-tree-item-container.profile-resource-child-container')); + const checkbox = disposables.add(new Checkbox('', false, defaultCheckboxStyles)); + append(container, checkbox.domNode); + const resourceLabel = disposables.add(this.labels.create(container, { hoverDelegate: this.hoverDelegate })); + return { checkbox, resourceLabel, disposables, elementDisposables: disposables.add(new DisposableStore()) }; + } + + renderElement({ element: profileResourceTreeElement }: ITreeNode, index: number, templateData: IProfileResourceChildTreeItemTemplateData, height: number | undefined): void { + templateData.elementDisposables.clear(); + const { element } = profileResourceTreeElement; + if (isString(element)) { + throw new Error('NewProfileResourceTreeRenderer can only render profile resource child tree items'); + } + if (element.checkbox) { + templateData.checkbox.domNode.classList.remove('hide'); + templateData.checkbox.checked = element.checkbox.isChecked; + templateData.checkbox.domNode.ariaLabel = element.checkbox.accessibilityInformation?.label ?? ''; + if (element.checkbox.accessibilityInformation?.role) { + templateData.checkbox.domNode.role = element.checkbox.accessibilityInformation.role; + } + } else { + templateData.checkbox.domNode.classList.add('hide'); + } + + const resource = URI.revive(element.resourceUri); + templateData.resourceLabel.setResource( + { + name: resource ? basename(resource) : element.label?.label, + description: isString(element.description) ? element.description : undefined, + resource + }, + { + forceLabel: true, + hideIcon: !resource, + }); + } + +} + +export class UserDataProfilesEditorInput extends EditorInput { + static readonly ID: string = 'workbench.input.userDataProfiles'; + readonly resource = undefined; + + private readonly model: UserDataProfilesEditorModel; + + constructor( + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(); + this.model = UserDataProfilesEditorModel.getInstance(this.instantiationService); + this._register(this.model.onDidChangeDirty(e => this._onDidChangeDirty.fire())); + } + + override get typeId(): string { return UserDataProfilesEditorInput.ID; } + override getName(): string { return localize('userDataProfiles', "Profiles"); } + override getIcon(): ThemeIcon | undefined { return defaultUserDataProfileIcon; } + + override async resolve(): Promise { + return this.model; + } + + override isDirty(): boolean { + return this.model.isDirty(); + } + + override async save(): Promise { + return this; + } + + override async revert(): Promise { + this.model.revert(); + } + + override matches(otherInput: EditorInput | IUntypedEditorInput): boolean { return otherInput instanceof UserDataProfilesEditorInput; } +} + +export class UserDataProfilesEditorInputSerializer implements IEditorSerializer { + canSerialize(editorInput: EditorInput): boolean { return true; } + serialize(editorInput: EditorInput): string { return ''; } + deserialize(instantiationService: IInstantiationService): EditorInput { return instantiationService.createInstance(UserDataProfilesEditorInput); } +} diff --git a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts new file mode 100644 index 00000000000..0650951cf64 --- /dev/null +++ b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts @@ -0,0 +1,602 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Action, IAction, Separator } from 'vs/base/common/actions'; +import { Emitter, Event } from 'vs/base/common/event'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { localize } from 'vs/nls'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { DidChangeProfilesEvent, isUserDataProfile, IUserDataProfile, IUserDataProfilesService, ProfileResourceType, ProfileResourceTypeFlags, toUserDataProfile, UseDefaultProfileFlags } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { IProfileResourceChildTreeItem, IUserDataProfileImportExportService, IUserDataProfileManagementService, IUserDataProfileService, IUserDataProfileTemplate } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { equals } from 'vs/base/common/objects'; +import { EditorModel } from 'vs/workbench/common/editor/editorModel'; +import { ExtensionsResourceExportTreeItem, ExtensionsResourceImportTreeItem } from 'vs/workbench/services/userDataProfile/browser/extensionsResource'; +import { SettingsResource, SettingsResourceTreeItem } from 'vs/workbench/services/userDataProfile/browser/settingsResource'; +import { KeybindingsResource, KeybindingsResourceTreeItem } from 'vs/workbench/services/userDataProfile/browser/keybindingsResource'; +import { TasksResource, TasksResourceTreeItem } from 'vs/workbench/services/userDataProfile/browser/tasksResource'; +import { SnippetsResource, SnippetsResourceTreeItem } from 'vs/workbench/services/userDataProfile/browser/snippetsResource'; +import { Codicon } from 'vs/base/common/codicons'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; +import { IFileService } from 'vs/platform/files/common/files'; +import { generateUuid } from 'vs/base/common/uuid'; + +export type ChangeEvent = { + readonly name?: boolean; + readonly icon?: boolean; + readonly flags?: boolean; + readonly active?: boolean; + readonly dirty?: boolean; + readonly message?: boolean; + readonly copyFrom?: boolean; + readonly copyFlags?: boolean; +}; + +export interface IProfileElement { + readonly onDidChange?: Event; + readonly name: string; + readonly icon?: string; + readonly flags?: UseDefaultProfileFlags; + readonly active?: boolean; + readonly dirty?: boolean; + readonly message?: string; +} + +export abstract class AbstractUserDataProfileElement extends Disposable { + + protected readonly _onDidChange = this._register(new Emitter()); + readonly onDidChange = this._onDidChange.event; + + constructor( + name: string, + icon: string | undefined, + flags: UseDefaultProfileFlags | undefined, + isActive: boolean, + isDirty: boolean, + @IUserDataProfilesService protected readonly userDataProfilesService: IUserDataProfilesService, + @IInstantiationService protected readonly instantiationService: IInstantiationService, + ) { + super(); + this._name = name; + this._icon = icon; + this._flags = flags; + this._active = isActive; + this._dirty = isDirty; + this._register(this.onDidChange(e => { + if (!e.dirty) { + this.dirty = this.hasUnsavedChanges(); + } + if (!e.message) { + this.validate(); + } + this.primaryAction.enabled = !this.message && this.dirty; + })); + } + + private _name = ''; + get name(): string { return this._name; } + set name(label: string) { + if (this._name !== label) { + this._name = label; + this._onDidChange.fire({ name: true }); + } + } + + private _icon: string | undefined; + get icon(): string | undefined { return this._icon; } + set icon(icon: string | undefined) { + if (this._icon !== icon) { + this._icon = icon; + this._onDidChange.fire({ icon: true }); + } + } + + private _flags: UseDefaultProfileFlags | undefined; + get flags(): UseDefaultProfileFlags | undefined { return this._flags; } + set flags(flags: UseDefaultProfileFlags | undefined) { + if (!equals(this._flags, flags)) { + this._flags = flags; + this._onDidChange.fire({ flags: true }); + } + } + + private _active: boolean = false; + get active(): boolean { return this._active; } + set active(active: boolean) { + if (this._active !== active) { + this._active = active; + this._onDidChange.fire({ active: true }); + } + } + + private _dirty: boolean = false; + get dirty(): boolean { return this._dirty; } + set dirty(isDirty: boolean) { + if (this._dirty !== isDirty) { + this._dirty = isDirty; + this._onDidChange.fire({ dirty: true }); + } + } + + private _message: string | undefined; + get message(): string | undefined { return this._message; } + set message(message: string | undefined) { + if (this._message !== message) { + this._message = message; + this._onDidChange.fire({ message: true }); + } + } + + getFlag(key: ProfileResourceType): boolean { + return this.flags?.[key] ?? false; + } + + setFlag(key: ProfileResourceType, value: boolean): void { + const flags = this.flags ? { ...this.flags } : {}; + if (value) { + flags[key] = true; + } else { + delete flags[key]; + } + this.flags = flags; + } + + validate(): void { + if (!this.dirty) { + this.message = undefined; + return; + } + if (!this.name) { + this.message = localize('profileNameRequired', "Profile name is required."); + return; + } + if (this.name !== this.getInitialName() && this.userDataProfilesService.profiles.some(p => p.name === this.name)) { + this.message = localize('profileExists', "Profile with name {0} already exists.", this.name); + return; + } + if ( + this.flags && this.flags.settings && this.flags.keybindings && this.flags.tasks && this.flags.snippets && this.flags.extensions + ) { + this.message = localize('invalid configurations', "The profile should contain at least one configuration."); + return; + } + this.message = undefined; + } + + async getChildren(resourceType: ProfileResourceType): Promise { + return []; + } + + reset(): void { } + + protected async getChildrenFromProfile(profile: IUserDataProfile, resourceType: ProfileResourceType): Promise { + profile = this.getFlag(resourceType) ? this.userDataProfilesService.defaultProfile : profile; + switch (resourceType) { + case ProfileResourceType.Settings: + return this.instantiationService.createInstance(SettingsResourceTreeItem, profile).getChildren(); + case ProfileResourceType.Keybindings: + return this.instantiationService.createInstance(KeybindingsResourceTreeItem, profile).getChildren(); + case ProfileResourceType.Snippets: + return (await this.instantiationService.createInstance(SnippetsResourceTreeItem, profile).getChildren()) ?? []; + case ProfileResourceType.Tasks: + return this.instantiationService.createInstance(TasksResourceTreeItem, profile).getChildren(); + case ProfileResourceType.Extensions: + return this.instantiationService.createInstance(ExtensionsResourceExportTreeItem, profile).getChildren(); + } + return []; + } + + protected getInitialName(): string { + return ''; + } + + abstract readonly primaryAction: Action; + abstract readonly secondaryActions: IAction[]; + protected abstract hasUnsavedChanges(): boolean; +} + +export class UserDataProfileElement extends AbstractUserDataProfileElement implements IProfileElement { + + get profile(): IUserDataProfile { return this._profile; } + + readonly primaryAction = new Action('userDataProfile.save', localize('save', "Save"), undefined, this.dirty, () => this.save()); + + constructor( + private _profile: IUserDataProfile, + readonly secondaryActions: IAction[], + @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, + @IUserDataProfileManagementService private readonly userDataProfileManagementService: IUserDataProfileManagementService, + @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super( + _profile.name, + _profile.icon, + _profile.useDefaultFlags, + userDataProfileService.currentProfile.id === _profile.id, + false, + userDataProfilesService, + instantiationService, + ); + this._register(this.userDataProfileService.onDidChangeCurrentProfile(() => this.active = this.userDataProfileService.currentProfile.id === this.profile.id)); + this._register(this.userDataProfilesService.onDidChangeProfiles(() => { + const profile = this.userDataProfilesService.profiles.find(p => p.id === this.profile.id); + if (profile) { + this._profile = profile; + this.name = profile.name; + this.icon = profile.icon; + this.flags = profile.useDefaultFlags; + } + })); + } + + protected hasUnsavedChanges(): boolean { + if (this.name !== this.profile.name) { + return true; + } + if (this.icon !== this.profile.icon) { + return true; + } + if (!equals(this.flags ?? {}, this.profile.useDefaultFlags ?? {})) { + return true; + } + return false; + } + + private async save(): Promise { + if (!this.dirty) { + return; + } + this.validate(); + if (this.message) { + return; + } + const useDefaultFlags: UseDefaultProfileFlags | undefined = this.flags + ? this.flags.settings && this.flags.keybindings && this.flags.tasks && this.flags.globalState && this.flags.extensions ? undefined : this.flags + : undefined; + + await this.userDataProfileManagementService.updateProfile(this.profile, { + name: this.name, + icon: this.icon, + useDefaultFlags: this.profile.useDefaultFlags && !useDefaultFlags ? {} : useDefaultFlags + }); + + this.dirty = false; + } + + override async getChildren(resourceType: ProfileResourceType): Promise { + return this.getChildrenFromProfile(this.profile, resourceType); + } + + override reset(): void { + this.name = this.profile.name; + this.icon = this.profile.icon; + this.flags = this.profile.useDefaultFlags; + } + + protected override getInitialName(): string { + return this.profile.name; + } + +} + +const USER_DATA_PROFILE_TEMPLATE_PREVIEW_SCHEME = 'userdataprofiletemplatepreview'; + +export class NewProfileElement extends AbstractUserDataProfileElement implements IProfileElement { + + constructor( + name: string, + copyFrom: URI | IUserDataProfile | undefined, + readonly primaryAction: Action, + readonly secondaryActions: Action[], + @IFileService private readonly fileService: IFileService, + @IUserDataProfileImportExportService private readonly userDataProfileImportExportService: IUserDataProfileImportExportService, + @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super( + name, + undefined, + undefined, + false, + true, + userDataProfilesService, + instantiationService, + ); + this._copyFrom = copyFrom; + this._copyFlags = this.getCopyFlagsFrom(copyFrom); + this._register(this.fileService.registerProvider(USER_DATA_PROFILE_TEMPLATE_PREVIEW_SCHEME, this._register(new InMemoryFileSystemProvider()))); + } + + private _copyFrom: IUserDataProfile | URI | undefined; + get copyFrom(): IUserDataProfile | URI | undefined { return this._copyFrom; } + set copyFrom(copyFrom: IUserDataProfile | URI | undefined) { + if (this._copyFrom !== copyFrom) { + this._copyFrom = copyFrom; + this._onDidChange.fire({ copyFrom: true }); + this.flags = undefined; + this.copyFlags = this.getCopyFlagsFrom(copyFrom); + } + } + + private _copyFlags: ProfileResourceTypeFlags | undefined; + get copyFlags(): ProfileResourceTypeFlags | undefined { return this._copyFlags; } + set copyFlags(flags: ProfileResourceTypeFlags | undefined) { + if (!equals(this._copyFlags, flags)) { + this._copyFlags = flags; + this._onDidChange.fire({ copyFlags: true }); + } + } + + private getCopyFlagsFrom(copyFrom: URI | IUserDataProfile | undefined): ProfileResourceTypeFlags | undefined { + return copyFrom ? { + settings: true, + keybindings: true, + snippets: true, + tasks: true, + extensions: true + } : undefined; + } + + getCopyFlag(key: ProfileResourceType): boolean { + return this.copyFlags?.[key] ?? false; + } + + setCopyFlag(key: ProfileResourceType, value: boolean): void { + const flags = this.copyFlags ? { ...this.copyFlags } : {}; + flags[key] = value; + this.copyFlags = flags; + } + + protected hasUnsavedChanges(): boolean { + return true; + } + + override async getChildren(resourceType: ProfileResourceType): Promise { + if (!this.getCopyFlag(resourceType)) { + return []; + } + if (this.copyFrom instanceof URI) { + const template = await this.userDataProfileImportExportService.resolveProfileTemplate(this.copyFrom); + if (!template) { + return []; + } + return this.getChildrenFromProfileTemplate(template, resourceType); + } + if (this.copyFrom) { + return this.getChildrenFromProfile(this.copyFrom, resourceType); + } + if (this.getFlag(resourceType)) { + return this.getChildrenFromProfile(this.userDataProfilesService.defaultProfile, resourceType); + } + return []; + } + + private async getChildrenFromProfileTemplate(profileTemplate: IUserDataProfileTemplate, resourceType: ProfileResourceType): Promise { + const profile = toUserDataProfile(generateUuid(), this.name, URI.file('/root').with({ scheme: USER_DATA_PROFILE_TEMPLATE_PREVIEW_SCHEME }), URI.file('/cache').with({ scheme: USER_DATA_PROFILE_TEMPLATE_PREVIEW_SCHEME })); + switch (resourceType) { + case ProfileResourceType.Settings: + if (profileTemplate.settings) { + await this.instantiationService.createInstance(SettingsResource).apply(profileTemplate.settings, profile); + } + return this.getChildrenFromProfile(profile, resourceType); + case ProfileResourceType.Keybindings: + if (profileTemplate.keybindings) { + await this.instantiationService.createInstance(KeybindingsResource).apply(profileTemplate.keybindings, profile); + } + return this.getChildrenFromProfile(profile, resourceType); + case ProfileResourceType.Snippets: + if (profileTemplate.snippets) { + await this.instantiationService.createInstance(SnippetsResource).apply(profileTemplate.snippets, profile); + } + return this.getChildrenFromProfile(profile, resourceType); + case ProfileResourceType.Tasks: + if (profileTemplate.tasks) { + await this.instantiationService.createInstance(TasksResource).apply(profileTemplate.tasks, profile); + } + return this.getChildrenFromProfile(profile, resourceType); + case ProfileResourceType.Extensions: + if (profileTemplate.extensions) { + return this.instantiationService.createInstance(ExtensionsResourceImportTreeItem, profileTemplate.extensions).getChildren(); + } + } + return []; + } +} + +export class UserDataProfilesEditorModel extends EditorModel { + + private static INSTANCE: UserDataProfilesEditorModel | undefined; + static getInstance(instantiationService: IInstantiationService): UserDataProfilesEditorModel { + if (!UserDataProfilesEditorModel.INSTANCE) { + UserDataProfilesEditorModel.INSTANCE = instantiationService.createInstance(UserDataProfilesEditorModel); + } + return UserDataProfilesEditorModel.INSTANCE; + } + + private _profiles: [AbstractUserDataProfileElement, DisposableStore][] = []; + get profiles(): AbstractUserDataProfileElement[] { + return this._profiles + .map(([profile]) => profile) + .sort((a, b) => { + if (a instanceof NewProfileElement) { + return 1; + } + if (b instanceof NewProfileElement) { + return -1; + } + if (a instanceof UserDataProfileElement && a.profile.isDefault) { + return -1; + } + if (b instanceof UserDataProfileElement && b.profile.isDefault) { + return 1; + } + return a.name.localeCompare(b.name); + }); + } + + private newProfileElement: NewProfileElement | undefined; + + private _onDidChange = this._register(new Emitter()); + readonly onDidChange = this._onDidChange.event; + + private _onDidChangeDirty = this._register(new Emitter()); + readonly onDidChangeDirty = this._onDidChangeDirty.event; + + constructor( + @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, + @IUserDataProfileManagementService private readonly userDataProfileManagementService: IUserDataProfileManagementService, + @IUserDataProfileImportExportService private readonly userDataProfileImportExportService: IUserDataProfileImportExportService, + @IDialogService private readonly dialogService: IDialogService, + @ITelemetryService private readonly telemetryService: ITelemetryService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(); + for (const profile of userDataProfilesService.profiles) { + this._profiles.push(this.createProfileElement(profile)); + } + this._register(toDisposable(() => this._profiles.splice(0, this._profiles.length).map(([, disposables]) => disposables.dispose()))); + this._register(userDataProfilesService.onDidChangeProfiles(e => this.onDidChangeProfiles(e))); + } + + private onDidChangeProfiles(e: DidChangeProfilesEvent): void { + for (const profile of e.added) { + if (profile.name !== this.newProfileElement?.name) { + this._profiles.push(this.createProfileElement(profile)); + } + } + for (const profile of e.removed) { + const index = this._profiles.findIndex(([p]) => p instanceof UserDataProfileElement && p.profile.id === profile.id); + if (index !== -1) { + this._profiles.splice(index, 1).map(([, disposables]) => disposables.dispose()); + } + } + this._onDidChange.fire(undefined); + } + + private createProfileElement(profile: IUserDataProfile): [UserDataProfileElement, DisposableStore] { + const disposables = new DisposableStore(); + const actions: IAction[] = []; + actions.push(new Action('userDataProfile.copyFromProfile', localize('copyFromProfile', "Save As..."), ThemeIcon.asClassName(Codicon.copy), true, () => this.createNewProfile(profile))); + actions.push(new Action('userDataProfile.export', localize('export', "Export..."), ThemeIcon.asClassName(Codicon.export), true, () => this.exportProfile(profile))); + if (!profile.isDefault) { + actions.push(new Separator()); + actions.push(new Action('userDataProfile.delete', localize('delete', "Delete"), ThemeIcon.asClassName(Codicon.trash), true, () => this.removeProfile(profile))); + } + const profileElement = disposables.add(this.instantiationService.createInstance(UserDataProfileElement, + profile, + actions + )); + disposables.add(profileElement.onDidChange(e => { + if (e.dirty) { + this._onDidChangeDirty.fire(this.isDirty()); + } + })); + return [profileElement, disposables]; + } + + createNewProfile(copyFrom?: URI | IUserDataProfile): IProfileElement { + if (!this.newProfileElement) { + const disposables = new DisposableStore(); + this.newProfileElement = disposables.add(this.instantiationService.createInstance(NewProfileElement, + localize('untitled', "Untitled"), + copyFrom, + new Action('userDataProfile.create', localize('create', "Create & Apply"), undefined, true, () => this.saveNewProfile()), + [ + new Action('userDataProfile.discard', localize('discard', "Discard"), ThemeIcon.asClassName(Codicon.trash), true, () => { + this.removeNewProfile(); + this._onDidChange.fire(undefined); + }) + ] + )); + this._profiles.push([this.newProfileElement, disposables]); + this._onDidChange.fire(this.newProfileElement); + } + return this.newProfileElement; + } + + isDirty(): boolean { + return this._profiles.some(([p]) => p.dirty); + } + + revert(): void { + this.removeNewProfile(); + for (const [profile] of this._profiles) { + profile.reset(); + } + this._onDidChangeDirty.fire(false); + this._onDidChange.fire(undefined); + } + + private removeNewProfile(): void { + if (this.newProfileElement) { + const index = this._profiles.findIndex(([p]) => p === this.newProfileElement); + if (index !== -1) { + this._profiles.splice(index, 1).map(([, disposables]) => disposables.dispose()); + } + this.newProfileElement = undefined; + } + } + + private async saveNewProfile(): Promise { + if (!this.newProfileElement) { + return; + } + this.newProfileElement.validate(); + if (this.newProfileElement.message) { + return; + } + const { flags, icon, name, copyFrom } = this.newProfileElement; + const useDefaultFlags: UseDefaultProfileFlags | undefined = flags + ? flags.settings && flags.keybindings && flags.tasks && flags.globalState && flags.extensions ? undefined : flags + : undefined; + + type CreateProfileInfoClassification = { + owner: 'sandy081'; + comment: 'Report when profile is about to be created'; + source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Type of profile source' }; + }; + type CreateProfileInfoEvent = { + source: string | undefined; + }; + const createProfileTelemetryData: CreateProfileInfoEvent = { source: copyFrom instanceof URI ? 'template' : isUserDataProfile(copyFrom) ? 'profile' : copyFrom ? 'external' : undefined }; + + if (copyFrom instanceof URI) { + this.telemetryService.publicLog2('userDataProfile.createFromTemplate', createProfileTelemetryData); + await this.userDataProfileImportExportService.importProfile(copyFrom, { mode: 'apply', name: name, useDefaultFlags, icon: icon ? icon : undefined, resourceTypeFlags: this.newProfileElement.copyFlags }); + } else if (isUserDataProfile(copyFrom)) { + this.telemetryService.publicLog2('userDataProfile.createFromProfile', createProfileTelemetryData); + await this.userDataProfileImportExportService.createFromProfile(copyFrom, name, { useDefaultFlags, icon: icon ? icon : undefined, resourceTypeFlags: this.newProfileElement.copyFlags }); + } else { + this.telemetryService.publicLog2('userDataProfile.createEmptyProfile', createProfileTelemetryData); + await this.userDataProfileManagementService.createAndEnterProfile(name, { useDefaultFlags, icon: icon ? icon : undefined }); + } + + this.removeNewProfile(); + const profile = this.userDataProfilesService.profiles.find(p => p.name === name); + if (profile) { + this.onDidChangeProfiles({ added: [profile], removed: [], updated: [], all: this.userDataProfilesService.profiles }); + } + } + + private async removeProfile(profile: IUserDataProfile): Promise { + const result = await this.dialogService.confirm({ + type: 'info', + message: localize('deleteProfile', "Are you sure you want to delete the profile '{0}'?", profile.name), + primaryButton: localize('delete', "Delete"), + cancelButton: localize('cancel', "Cancel") + }); + if (result.confirmed) { + await this.userDataProfileManagementService.removeProfile(profile); + } + } + + private async exportProfile(profile: IUserDataProfile): Promise { + return this.userDataProfileImportExportService.exportProfile2(profile); + } +} diff --git a/src/vs/workbench/contrib/userDataProfile/common/userDataProfile.ts b/src/vs/workbench/contrib/userDataProfile/common/userDataProfile.ts new file mode 100644 index 00000000000..d816f4ee1c3 --- /dev/null +++ b/src/vs/workbench/contrib/userDataProfile/common/userDataProfile.ts @@ -0,0 +1,10 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IEditorPane } from 'vs/workbench/common/editor'; + +export interface IUserDataProfilesEditor extends IEditorPane { + +} diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index 632ee6ad4e3..c2de392a001 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -730,15 +730,15 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo f1: true, precondition: when, menu: [{ - group: '3_settings_sync', + group: '3_configuration', id: MenuId.GlobalActivity, when, - order: 1 + order: 2 }, { - group: '3_settings_sync', + group: '3_configuration', id: MenuId.MenubarPreferencesMenu, when, - order: 1 + order: 2 }, { group: '1_settings', id: MenuId.AccountsContext, @@ -762,7 +762,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo title: localize('turnin on sync', "Turning on Settings Sync..."), precondition: ContextKeyExpr.false(), menu: [{ - group: '3_settings_sync', + group: '3_configuration', id: MenuId.GlobalActivity, when, order: 2 @@ -809,7 +809,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo id: 'workbench.userData.actions.signin', title: localize('sign in global', "Sign in to Sync Settings"), menu: { - group: '3_settings_sync', + group: '3_configuration', id: MenuId.GlobalActivity, when, order: 2 @@ -851,12 +851,12 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo f1: true, precondition: CONTEXT_HAS_CONFLICTS, menu: [{ - group: '3_settings_sync', + group: '3_configuration', id: MenuId.GlobalActivity, when: CONTEXT_HAS_CONFLICTS, order: 2 }, { - group: '3_settings_sync', + group: '3_configuration', id: MenuId.MenubarPreferencesMenu, when: CONTEXT_HAS_CONFLICTS, order: 2 @@ -881,13 +881,13 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo menu: [ { id: MenuId.GlobalActivity, - group: '3_settings_sync', + group: '3_configuration', when, order: 2 }, { id: MenuId.MenubarPreferencesMenu, - group: '3_settings_sync', + group: '3_configuration', when, order: 2, }, diff --git a/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts b/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts index 34e9694bd42..13b592b7e0e 100644 --- a/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts +++ b/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts @@ -10,7 +10,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { INotificationService } from 'vs/platform/notification/common/notification'; import { Emitter, Event } from 'vs/base/common/event'; import * as DOM from 'vs/base/browser/dom'; -import { IUserDataProfileImportExportService, PROFILE_FILTER, PROFILE_EXTENSION, IUserDataProfileContentHandler, IS_PROFILE_IMPORT_IN_PROGRESS_CONTEXT, PROFILES_TITLE, defaultUserDataProfileIcon, IUserDataProfileService, IProfileResourceTreeItem, PROFILES_CATEGORY, IUserDataProfileManagementService, IS_PROFILE_EXPORT_IN_PROGRESS_CONTEXT, ISaveProfileResult, IProfileImportOptions, PROFILE_URL_AUTHORITY, toUserDataProfileUri } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { IUserDataProfileImportExportService, PROFILE_FILTER, PROFILE_EXTENSION, IUserDataProfileContentHandler, IS_PROFILE_IMPORT_IN_PROGRESS_CONTEXT, PROFILES_TITLE, defaultUserDataProfileIcon, IUserDataProfileService, IProfileResourceTreeItem, PROFILES_CATEGORY, IUserDataProfileManagementService, IS_PROFILE_EXPORT_IN_PROGRESS_CONTEXT, ISaveProfileResult, IProfileImportOptions, PROFILE_URL_AUTHORITY, toUserDataProfileUri, IUserDataProfileCreateOptions } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { IDialogService, IFileDialogService, IPromptButton } from 'vs/platform/dialogs/common/dialogs'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; @@ -19,7 +19,7 @@ import { IFileService } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; import { Extensions, ITreeItem, ITreeViewDataProvider, ITreeViewDescriptor, IViewContainersRegistry, IViewDescriptorService, IViewsRegistry, TreeItemCollapsibleState, ViewContainer, ViewContainerLocation } from 'vs/workbench/common/views'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; -import { IUserDataProfile, IUserDataProfileOptions, IUserDataProfilesService, ProfileResourceType, UseDefaultProfileFlags, isUserDataProfile, toUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { IUserDataProfile, IUserDataProfileOptions, IUserDataProfilesService, ProfileResourceType, ProfileResourceTypeFlags, UseDefaultProfileFlags, isUserDataProfile, toUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; import { ContextKeyExpr, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { Registry } from 'vs/platform/registry/common/platform'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; @@ -550,12 +550,12 @@ export class UserDataProfileImportExportService extends Disposable implements IU } const disposables = new DisposableStore(); try { - const userDataProfilesExportState = disposables.add(this.instantiationService.createInstance(UserDataProfileExportState, this.userDataProfileService.currentProfile)); + const userDataProfilesExportState = disposables.add(this.instantiationService.createInstance(UserDataProfileExportState, this.userDataProfileService.currentProfile, undefined)); const barrier = new Barrier(); const exportAction = new BarrierAction(barrier, new Action('export', localize('export', "Export"), undefined, true, async () => { exportAction.enabled = false; try { - await this.doExportProfile(userDataProfilesExportState); + await this.doExportProfile(userDataProfilesExportState, EXPORT_PROFILE_PREVIEW_VIEW); } catch (error) { exportAction.enabled = true; this.notificationService.error(error); @@ -572,8 +572,18 @@ export class UserDataProfileImportExportService extends Disposable implements IU } } - private async createFromProfile(profile: IUserDataProfile, name: string, options?: IUserDataProfileOptions): Promise { - const userDataProfilesExportState = this.instantiationService.createInstance(UserDataProfileExportState, profile); + async exportProfile2(profile: IUserDataProfile): Promise { + const disposables = new DisposableStore(); + try { + const userDataProfilesExportState = disposables.add(this.instantiationService.createInstance(UserDataProfileExportState, profile, undefined)); + await this.doExportProfile(userDataProfilesExportState, ProgressLocation.Notification); + } finally { + disposables.dispose(); + } + } + + async createFromProfile(profile: IUserDataProfile, name: string, options?: IUserDataProfileCreateOptions): Promise { + const userDataProfilesExportState = this.instantiationService.createInstance(UserDataProfileExportState, profile, options?.resourceTypeFlags); try { const profileTemplate = await userDataProfilesExportState.getProfileTemplate(name, options?.icon); await this.progressService.withProgress({ @@ -584,8 +594,10 @@ export class UserDataProfileImportExportService extends Disposable implements IU const reportProgress = (message: string) => progress.report({ message: localize('create from profile', "Create Profile: {0}", message) }); const createdProfile = await this.doCreateProfile(profileTemplate, false, false, { useDefaultFlags: options?.useDefaultFlags, icon: options?.icon }, reportProgress); if (createdProfile) { - reportProgress(localize('progress extensions', "Applying Extensions...")); - await this.instantiationService.createInstance(ExtensionsResource).copy(profile, createdProfile, false); + if (options?.resourceTypeFlags?.extensions ?? true) { + reportProgress(localize('progress extensions', "Applying Extensions...")); + await this.instantiationService.createInstance(ExtensionsResource).copy(profile, createdProfile, false); + } reportProgress(localize('switching profile', "Switching Profile...")); await this.userDataProfileManagementService.switchProfile(createdProfile); @@ -597,7 +609,7 @@ export class UserDataProfileImportExportService extends Disposable implements IU } async createTroubleshootProfile(): Promise { - const userDataProfilesExportState = this.instantiationService.createInstance(UserDataProfileExportState, this.userDataProfileService.currentProfile); + const userDataProfilesExportState = this.instantiationService.createInstance(UserDataProfileExportState, this.userDataProfileService.currentProfile, undefined); try { const profileTemplate = await userDataProfilesExportState.getProfileTemplate(localize('troubleshoot issue', "Troubleshoot Issue"), undefined); await this.progressService.withProgress({ @@ -620,7 +632,7 @@ export class UserDataProfileImportExportService extends Disposable implements IU } } - private async doExportProfile(userDataProfilesExportState: UserDataProfileExportState): Promise { + private async doExportProfile(userDataProfilesExportState: UserDataProfileExportState, location: ProgressLocation | string): Promise { const profile = await userDataProfilesExportState.getProfileToExport(); if (!profile) { return; @@ -632,7 +644,7 @@ export class UserDataProfileImportExportService extends Disposable implements IU try { await this.progressService.withProgress({ - location: EXPORT_PROFILE_PREVIEW_VIEW, + location, title: localize('profiles.exporting', "{0}: Exporting...", PROFILES_CATEGORY.value), }, async progress => { const id = await this.pickProfileContentHandler(profile.name); @@ -685,7 +697,7 @@ export class UserDataProfileImportExportService extends Disposable implements IU } } - private async resolveProfileTemplate(uri: URI, options?: IProfileImportOptions): Promise { + async resolveProfileTemplate(uri: URI, options?: IProfileImportOptions): Promise { const profileContent = await this.resolveProfileContent(uri); if (profileContent === null) { return null; @@ -704,6 +716,30 @@ export class UserDataProfileImportExportService extends Disposable implements IU profileTemplate.icon = options.icon; } + if (options?.resourceTypeFlags?.settings === false) { + profileTemplate.settings = undefined; + } + + if (options?.resourceTypeFlags?.keybindings === false) { + profileTemplate.keybindings = undefined; + } + + if (options?.resourceTypeFlags?.snippets === false) { + profileTemplate.snippets = undefined; + } + + if (options?.resourceTypeFlags?.tasks === false) { + profileTemplate.tasks = undefined; + } + + if (options?.resourceTypeFlags?.globalState === false) { + profileTemplate.globalState = undefined; + } + + if (options?.resourceTypeFlags?.extensions === false) { + profileTemplate.extensions = undefined; + } + return profileTemplate; } @@ -1349,6 +1385,7 @@ class UserDataProfileExportState extends UserDataProfileImportExportState { constructor( readonly profile: IUserDataProfile, + private readonly exportFlags: ProfileResourceTypeFlags | undefined, @IQuickInputService quickInputService: IQuickInputService, @IFileService private readonly fileService: IFileService, @IInstantiationService private readonly instantiationService: IInstantiationService @@ -1364,49 +1401,61 @@ class UserDataProfileExportState extends UserDataProfileImportExportState { const roots: IProfileResourceTreeItem[] = []; const exportPreviewProfle = this.createExportPreviewProfile(this.profile); - const settingsResource = this.instantiationService.createInstance(SettingsResource); - const settingsContent = await settingsResource.getContent(this.profile); - await settingsResource.apply(settingsContent, exportPreviewProfle); - const settingsResourceTreeItem = this.instantiationService.createInstance(SettingsResourceTreeItem, exportPreviewProfle); - if (await settingsResourceTreeItem.hasContent()) { - roots.push(settingsResourceTreeItem); + if (this.exportFlags?.settings ?? true) { + const settingsResource = this.instantiationService.createInstance(SettingsResource); + const settingsContent = await settingsResource.getContent(this.profile); + await settingsResource.apply(settingsContent, exportPreviewProfle); + const settingsResourceTreeItem = this.instantiationService.createInstance(SettingsResourceTreeItem, exportPreviewProfle); + if (await settingsResourceTreeItem.hasContent()) { + roots.push(settingsResourceTreeItem); + } } - const keybindingsResource = this.instantiationService.createInstance(KeybindingsResource); - const keybindingsContent = await keybindingsResource.getContent(this.profile); - await keybindingsResource.apply(keybindingsContent, exportPreviewProfle); - const keybindingsResourceTreeItem = this.instantiationService.createInstance(KeybindingsResourceTreeItem, exportPreviewProfle); - if (await keybindingsResourceTreeItem.hasContent()) { - roots.push(keybindingsResourceTreeItem); + if (this.exportFlags?.keybindings ?? true) { + const keybindingsResource = this.instantiationService.createInstance(KeybindingsResource); + const keybindingsContent = await keybindingsResource.getContent(this.profile); + await keybindingsResource.apply(keybindingsContent, exportPreviewProfle); + const keybindingsResourceTreeItem = this.instantiationService.createInstance(KeybindingsResourceTreeItem, exportPreviewProfle); + if (await keybindingsResourceTreeItem.hasContent()) { + roots.push(keybindingsResourceTreeItem); + } } - const snippetsResource = this.instantiationService.createInstance(SnippetsResource); - const snippetsContent = await snippetsResource.getContent(this.profile); - await snippetsResource.apply(snippetsContent, exportPreviewProfle); - const snippetsResourceTreeItem = this.instantiationService.createInstance(SnippetsResourceTreeItem, exportPreviewProfle); - if (await snippetsResourceTreeItem.hasContent()) { - roots.push(snippetsResourceTreeItem); + if (this.exportFlags?.snippets ?? true) { + const snippetsResource = this.instantiationService.createInstance(SnippetsResource); + const snippetsContent = await snippetsResource.getContent(this.profile); + await snippetsResource.apply(snippetsContent, exportPreviewProfle); + const snippetsResourceTreeItem = this.instantiationService.createInstance(SnippetsResourceTreeItem, exportPreviewProfle); + if (await snippetsResourceTreeItem.hasContent()) { + roots.push(snippetsResourceTreeItem); + } } - const tasksResource = this.instantiationService.createInstance(TasksResource); - const tasksContent = await tasksResource.getContent(this.profile); - await tasksResource.apply(tasksContent, exportPreviewProfle); - const tasksResourceTreeItem = this.instantiationService.createInstance(TasksResourceTreeItem, exportPreviewProfle); - if (await tasksResourceTreeItem.hasContent()) { - roots.push(tasksResourceTreeItem); + if (this.exportFlags?.tasks ?? true) { + const tasksResource = this.instantiationService.createInstance(TasksResource); + const tasksContent = await tasksResource.getContent(this.profile); + await tasksResource.apply(tasksContent, exportPreviewProfle); + const tasksResourceTreeItem = this.instantiationService.createInstance(TasksResourceTreeItem, exportPreviewProfle); + if (await tasksResourceTreeItem.hasContent()) { + roots.push(tasksResourceTreeItem); + } } - const globalStateResource = joinPath(exportPreviewProfle.globalStorageHome, 'globalState.json').with({ scheme: USER_DATA_PROFILE_EXPORT_PREVIEW_SCHEME }); - const globalStateResourceTreeItem = this.instantiationService.createInstance(GlobalStateResourceExportTreeItem, exportPreviewProfle, globalStateResource); - const content = await globalStateResourceTreeItem.getContent(); - if (content) { - await this.fileService.writeFile(globalStateResource, VSBuffer.fromString(JSON.stringify(JSON.parse(content), null, '\t'))); - roots.push(globalStateResourceTreeItem); + if (this.exportFlags?.globalState ?? true) { + const globalStateResource = joinPath(exportPreviewProfle.globalStorageHome, 'globalState.json').with({ scheme: USER_DATA_PROFILE_EXPORT_PREVIEW_SCHEME }); + const globalStateResourceTreeItem = this.instantiationService.createInstance(GlobalStateResourceExportTreeItem, exportPreviewProfle, globalStateResource); + const content = await globalStateResourceTreeItem.getContent(); + if (content) { + await this.fileService.writeFile(globalStateResource, VSBuffer.fromString(JSON.stringify(JSON.parse(content), null, '\t'))); + roots.push(globalStateResourceTreeItem); + } } - const extensionsResourceTreeItem = this.instantiationService.createInstance(ExtensionsResourceExportTreeItem, exportPreviewProfle); - if (await extensionsResourceTreeItem.hasContent()) { - roots.push(extensionsResourceTreeItem); + if (this.exportFlags?.extensions ?? true) { + const extensionsResourceTreeItem = this.instantiationService.createInstance(ExtensionsResourceExportTreeItem, exportPreviewProfle); + if (await extensionsResourceTreeItem.hasContent()) { + roots.push(extensionsResourceTreeItem); + } } previewFileSystemProvider.setReadOnly(true); diff --git a/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts b/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts index 7f0b3b60418..d2c5cb4487e 100644 --- a/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts +++ b/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts @@ -8,7 +8,7 @@ import { Event } from 'vs/base/common/event'; import { localize, localize2 } from 'vs/nls'; import { MenuId } from 'vs/platform/actions/common/actions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IUserDataProfile, IUserDataProfileOptions, IUserDataProfileUpdateOptions, ProfileResourceType } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { IUserDataProfile, IUserDataProfileOptions, IUserDataProfileUpdateOptions, ProfileResourceType, ProfileResourceTypeFlags } from 'vs/platform/userDataProfile/common/userDataProfile'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { URI } from 'vs/base/common/uri'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; @@ -82,6 +82,11 @@ export interface IProfileImportOptions extends IUserDataProfileOptions { readonly name?: string; readonly icon?: string; readonly mode?: 'preview' | 'apply' | 'both'; + readonly resourceTypeFlags?: ProfileResourceTypeFlags; +} + +export interface IUserDataProfileCreateOptions extends IUserDataProfileOptions { + readonly resourceTypeFlags?: ProfileResourceTypeFlags; } export const IUserDataProfileImportExportService = createDecorator('IUserDataProfileImportExportService'); @@ -91,10 +96,13 @@ export interface IUserDataProfileImportExportService { registerProfileContentHandler(id: string, profileContentHandler: IUserDataProfileContentHandler): IDisposable; unregisterProfileContentHandler(id: string): void; + resolveProfileTemplate(uri: URI): Promise; exportProfile(): Promise; + exportProfile2(profile: IUserDataProfile): Promise; importProfile(uri: URI, options?: IProfileImportOptions): Promise; showProfileContents(): Promise; createProfile(from?: IUserDataProfile | URI): Promise; + createFromProfile(from: IUserDataProfile, name: string, options?: IUserDataProfileCreateOptions): Promise; editProfile(profile: IUserDataProfile): Promise; createTroubleshootProfile(): Promise; setProfile(profile: IUserDataProfileTemplate): Promise; From 6193553bfeaa9f20c5164b79e941fe97a5f7cd5a Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 22 May 2024 13:08:51 -0700 Subject: [PATCH 325/357] testing: polish followups a little, selfhost with copilot --- .../src/extension.ts | 19 +++--- .../api/browser/mainThreadTesting.ts | 9 ++- .../contrib/testing/browser/media/testing.css | 4 ++ .../testing/browser/testingOutputPeek.ts | 60 +++++++++++++++---- .../contrib/testing/common/testService.ts | 8 +++ .../contrib/testing/common/testServiceImpl.ts | 15 ++++- 6 files changed, 86 insertions(+), 29 deletions(-) diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts index d22cb023c67..960dbcf634e 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts @@ -41,16 +41,15 @@ export async function activate(context: vscode.ExtensionContext) { const ctrl = vscode.tests.createTestController('selfhost-test-controller', 'VS Code Tests'); const fileChangedEmitter = new vscode.EventEmitter(); - // todo@connor4312: tidy this up and make it work - // context.subscriptions.push(vscode.tests.registerTestFollowupProvider({ - // async provideFollowup(result, test, taskIndex, messageIndex, token) { - // await new Promise(r => setTimeout(r, 2000)); - // return [{ - // title: '$(sparkle) Ask copilot for help', - // command: 'asdf' - // }]; - // }, - // })); + context.subscriptions.push(vscode.tests.registerTestFollowupProvider({ + async provideFollowup(_result, test, taskIndex, messageIndex, _token) { + return [{ + title: '$(sparkle) Ask copilot for help', + command: 'github.copilot.tests.fixTestFailure', + arguments: [{ source: 'peekFollowup', test, message: test.taskStates[taskIndex].messages[messageIndex] }] + }]; + }, + })); ctrl.resolveHandler = async test => { diff --git a/src/vs/workbench/api/browser/mainThreadTesting.ts b/src/vs/workbench/api/browser/mainThreadTesting.ts index 0402df1b5fb..a3641b6687a 100644 --- a/src/vs/workbench/api/browser/mainThreadTesting.ts +++ b/src/vs/workbench/api/browser/mainThreadTesting.ts @@ -44,6 +44,12 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh super(); this.proxy = extHostContext.getProxy(ExtHostContext.ExtHostTesting); + this._register(this.testService.registerExtHost({ + provideTestFollowups: (req, token) => this.proxy.$provideTestFollowups(req, token), + executeTestFollowup: id => this.proxy.$executeTestFollowup(id), + disposeTestFollowups: ids => this.proxy.$disposeTestFollowups(ids), + })); + this._register(this.testService.onDidCancelTestRun(({ runId }) => { this.proxy.$cancelExtensionTestRun(runId); })); @@ -233,9 +239,6 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh runTests: (reqs, token) => this.proxy.$runControllerTests(reqs, token), startContinuousRun: (reqs, token) => this.proxy.$startContinuousRun(reqs, token), expandTest: (testId, levels) => this.proxy.$expandTest(testId, isFinite(levels) ? levels : -1), - provideTestFollowups: (req, token) => this.proxy.$provideTestFollowups(req, token), - executeTestFollowup: id => this.proxy.$executeTestFollowup(id), - disposeTestFollowups: ids => this.proxy.$disposeTestFollowups(ids), }; disposable.add(toDisposable(() => this.testProfiles.removeProfile(controllerId))); diff --git a/src/vs/workbench/contrib/testing/browser/media/testing.css b/src/vs/workbench/contrib/testing/browser/media/testing.css index e37cf2c6815..5fa30f14ebd 100644 --- a/src/vs/workbench/contrib/testing/browser/media/testing.css +++ b/src/vs/workbench/contrib/testing/browser/media/testing.css @@ -277,6 +277,9 @@ overflow: hidden; pointer-events: none; background: linear-gradient(transparent, var(--vscode-peekViewEditor-background) 50%); + display: flex; + align-items: center; + gap: 14px; &.animated { animation: fadeIn 150ms ease-out; @@ -289,6 +292,7 @@ cursor: pointer; pointer-events: auto; width: fit-content; + flex-shrink: 0; &, .codicon { color: var(--vscode-textLink-foreground); diff --git a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts index afbb0811a1c..a05b7d50fe0 100644 --- a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts @@ -69,6 +69,7 @@ import { WorkbenchCompressibleObjectTree } from 'vs/platform/list/browser/listSe import { INotificationService } from 'vs/platform/notification/common/notification'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IProgressService } from 'vs/platform/progress/common/progress'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; @@ -780,6 +781,7 @@ class FollowupActionWidget extends Disposable { constructor( private readonly container: HTMLElement, @ITestService private readonly testService: ITestService, + @IQuickInputService private readonly quickInput: IQuickInputService, ) { super(); } @@ -794,6 +796,12 @@ class FollowupActionWidget extends Disposable { private async showMessage(subject: MessageSubject) { const cts = this.visibleStore.add(new CancellationTokenSource()); const start = Date.now(); + + // Wait for completion otherwise results will not be available to the ext host: + if (subject.result instanceof LiveTestResult && !subject.result.completedAt) { + await new Promise(r => Event.once((subject.result as LiveTestResult).onComplete)(r)); + } + const followups = await this.testService.provideTestFollowups({ extId: subject.test.extId, messageIndex: subject.messageIndex, @@ -811,20 +819,10 @@ class FollowupActionWidget extends Disposable { dom.clearNode(this.el.root); this.el.root.classList.toggle('animated', Date.now() - start > FOLLOWUP_ANIMATION_MIN_TIME); - for (const fu of followups.followups) { - const link = document.createElement('a'); - link.tabIndex = 0; - dom.reset(link, ...renderLabelWithIcons(fu.message)); - this.visibleStore.add(dom.addDisposableListener(link, 'click', () => this.actionFollowup(link, fu))); - this.visibleStore.add(dom.addDisposableListener(link, 'keydown', e => { - const event = new StandardKeyboardEvent(e); - if (event.equals(KeyCode.Space) || event.equals(KeyCode.Enter)) { - this.actionFollowup(link, fu); - } - })); - - this.el.root.appendChild(link); + this.el.root.appendChild(this.makeFollowupLink(followups.followups[0])); + if (followups.followups.length > 1) { + this.el.root.appendChild(this.makeMoreLink(followups.followups)); } this.container.appendChild(this.el.root); @@ -833,6 +831,42 @@ class FollowupActionWidget extends Disposable { })); } + private makeFollowupLink(first: ITestFollowup) { + const link = this.makeLink(() => this.actionFollowup(link, first)); + dom.reset(link, ...renderLabelWithIcons(first.message)); + return link; + } + + private makeMoreLink(followups: ITestFollowup[]) { + const link = this.makeLink(() => + this.quickInput.pick(followups.map((f, i) => ({ + label: f.message, + index: i + }))).then(picked => { + if (picked?.length) { + followups[picked[0].index].execute(); + } + }) + ); + + link.innerText = localize('testFollowup.more', '+{0} More...', followups.length - 1); + return link; + } + + private makeLink(onClick: () => void) { + const link = document.createElement('a'); + link.tabIndex = 0; + this.visibleStore.add(dom.addDisposableListener(link, 'click', onClick)); + this.visibleStore.add(dom.addDisposableListener(link, 'keydown', e => { + const event = new StandardKeyboardEvent(e); + if (event.equals(KeyCode.Space) || event.equals(KeyCode.Enter)) { + onClick(); + } + })); + + return link; + } + private actionFollowup(link: HTMLAnchorElement, fu: ITestFollowup) { if (link.ariaDisabled !== 'true') { link.ariaDisabled = 'true'; diff --git a/src/vs/workbench/contrib/testing/common/testService.ts b/src/vs/workbench/contrib/testing/common/testService.ts index 09f008fa4fe..d3026db22eb 100644 --- a/src/vs/workbench/contrib/testing/common/testService.ts +++ b/src/vs/workbench/contrib/testing/common/testService.ts @@ -29,6 +29,9 @@ export interface IMainThreadTestController { expandTest(id: string, levels: number): Promise; startContinuousRun(request: ICallProfileRunHandler[], token: CancellationToken): Promise; runTests(request: IStartControllerTests[], token: CancellationToken): Promise; +} + +export interface IMainThreadTestHostProxy { provideTestFollowups(req: TestMessageFollowupRequest, token: CancellationToken): Promise; executeTestFollowup(id: number): Promise; disposeTestFollowups(ids: number[]): void; @@ -273,6 +276,11 @@ export interface ITestService { */ readonly showInlineOutput: MutableObservableValue; + /** + * Registers an interface that represents an extension host.. + */ + registerExtHost(controller: IMainThreadTestHostProxy): IDisposable; + /** * Registers an interface that runs tests for the given provider ID. */ diff --git a/src/vs/workbench/contrib/testing/common/testServiceImpl.ts b/src/vs/workbench/contrib/testing/common/testServiceImpl.ts index 2c34de5858e..fd3ffcd0999 100644 --- a/src/vs/workbench/contrib/testing/common/testServiceImpl.ts +++ b/src/vs/workbench/contrib/testing/common/testServiceImpl.ts @@ -27,13 +27,14 @@ import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingC import { canUseProfileWithTest, ITestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService'; import { ITestResult } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; -import { AmbiguousRunTestsRequest, IMainThreadTestController, ITestFollowups, ITestService } from 'vs/workbench/contrib/testing/common/testService'; +import { AmbiguousRunTestsRequest, IMainThreadTestController, IMainThreadTestHostProxy, ITestFollowups, ITestService } from 'vs/workbench/contrib/testing/common/testService'; import { ResolvedTestRunRequest, TestDiffOpType, TestMessageFollowupRequest, TestsDiff } from 'vs/workbench/contrib/testing/common/testTypes'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; export class TestService extends Disposable implements ITestService { declare readonly _serviceBrand: undefined; private testControllers = new Map(); + private testExtHosts = new Set(); private readonly cancelExtensionTestRunEmitter = new Emitter<{ runId: string | undefined }>(); private readonly willProcessDiffEmitter = new Emitter(); @@ -268,8 +269,8 @@ export class TestService extends Disposable implements ITestService { * @inheritdoc */ public async provideTestFollowups(req: TestMessageFollowupRequest, token: CancellationToken): Promise { - const reqs = await Promise.all([...this.testControllers.values()] - .map(async ctrl => ({ ctrl, followups: await ctrl.provideTestFollowups(req, token) }))); + const reqs = await Promise.all([...this.testExtHosts].map(async ctrl => + ({ ctrl, followups: await ctrl.provideTestFollowups(req, token) }))); const followups: ITestFollowups = { followups: reqs.flatMap(({ ctrl, followups }) => followups.map(f => ({ @@ -351,6 +352,14 @@ export class TestService extends Disposable implements ITestService { this.isRefreshingTests.set(false); } + /** + * @inheritdoc + */ + registerExtHost(controller: IMainThreadTestHostProxy): IDisposable { + this.testExtHosts.add(controller); + return toDisposable(() => this.testExtHosts.delete(controller)); + } + /** * @inheritdoc */ From 85fe2e2daf442b3bbf2fbcaabb2ff37cdd19c438 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Wed, 22 May 2024 13:10:07 -0700 Subject: [PATCH 326/357] Update for latest code mapper proposal (#213256) --- .../mappedCodeEditProvider.ts | 6 +-- .../src/tsServer/protocol/protocol.d.ts | 44 ++++++++----------- 2 files changed, 22 insertions(+), 28 deletions(-) diff --git a/extensions/typescript-language-features/src/languageFeatures/mappedCodeEditProvider.ts b/extensions/typescript-language-features/src/languageFeatures/mappedCodeEditProvider.ts index 0d04d2dc808..06ce5557b6c 100644 --- a/extensions/typescript-language-features/src/languageFeatures/mappedCodeEditProvider.ts +++ b/extensions/typescript-language-features/src/languageFeatures/mappedCodeEditProvider.ts @@ -27,8 +27,8 @@ class TsMappedEditsProvider implements vscode.MappedEditsProvider { } const response = await this.client.execute('mapCode', { - mappings: [{ - file, + file, + mapping: { contents: codeBlocks, focusLocations: context.documents.map(documents => { return documents.flatMap((contextItem): FileSpan[] => { @@ -39,7 +39,7 @@ class TsMappedEditsProvider implements vscode.MappedEditsProvider { return contextItem.ranges.map((range): FileSpan => ({ file, ...Range.toTextSpan(range) })); }); }), - }], + } }, token); if (response.type !== 'response' || !response.body) { return; diff --git a/extensions/typescript-language-features/src/tsServer/protocol/protocol.d.ts b/extensions/typescript-language-features/src/tsServer/protocol/protocol.d.ts index 45e09d63481..aa9b0589d2d 100644 --- a/extensions/typescript-language-features/src/tsServer/protocol/protocol.d.ts +++ b/extensions/typescript-language-features/src/tsServer/protocol/protocol.d.ts @@ -20,42 +20,36 @@ declare module '../../../../node_modules/typescript/lib/typescript' { readonly _serverType?: ServerType; } - export interface MapCodeRequestArgs { - /// The files and changes to try and apply/map. - mappings: MapCodeRequestDocumentMapping[]; - - /// Edits to apply to the current workspace before performing the mapping. - updates?: FileCodeEdits[] + export interface MapCodeRequestArgs extends FileRequestArgs { + /** + * The files and changes to try and apply/map. + */ + mapping: MapCodeRequestDocumentMapping; } export interface MapCodeRequestDocumentMapping { - /// The file for the request (absolute pathname required). Null/undefined - /// if specific file is unknown. - file?: string; - - /// Optional name of project that contains file - projectFileName?: string; - - /// The specific code to map/insert/replace in the file. + /** + * The specific code to map/insert/replace in the file. + */ contents: string[]; - /// Areas of "focus" to inform the code mapper with. For example, cursor - /// location, current selection, viewport, etc. Nested arrays denote - /// priority: toplevel arrays are more important than inner arrays, and - /// inner array priorities are based on items within that array. Items - /// earlier in the arrays have higher priority. - focusLocations?: FileSpan[][]; + /** + * Areas of "focus" to inform the code mapper with. For example, cursor + * location, current selection, viewport, etc. Nested arrays denote + * priority: toplevel arrays are more important than inner arrays, and + * inner array priorities are based on items within that array. Items + * earlier in the arrays have higher priority. + */ + focusLocations?: TextSpan[][]; } - export interface MapCodeRequest extends Request { - command: 'mapCode', + export interface MapCodeRequest extends FileRequest { + command: 'mapCode'; arguments: MapCodeRequestArgs; } export interface MapCodeResponse extends Response { - body: FileCodeEdits[] + body: readonly FileCodeEdits[]; } } } - - From d3a701fbc86f09bc8d36377657265dfe51cea09b Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Wed, 22 May 2024 13:42:52 -0700 Subject: [PATCH 327/357] Remove notebook chat menu --- .../browser/controller/chat/notebookChatController.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts index a781640e809..e5c83779451 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts @@ -26,6 +26,7 @@ import { ICursorStateComputer, ITextModel } from 'vs/editor/common/model'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { IModelService } from 'vs/editor/common/services/model'; import { localize } from 'vs/nls'; +import { MenuId } from 'vs/platform/actions/common/actions'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; @@ -37,8 +38,9 @@ import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; import { ProgressingEditsOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatStrategies'; import { InlineChatWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; import { asProgressiveEdit, performAsyncTextEdit } from 'vs/workbench/contrib/inlineChat/browser/utils'; +import { MENU_INLINE_CHAT_WIDGET } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { insertCell, runDeleteAction } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations'; -import { CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST, CTX_NOTEBOOK_CHAT_OUTER_FOCUS_POSITION, CTX_NOTEBOOK_CHAT_USER_DID_EDIT, MENU_CELL_CHAT_INPUT, MENU_CELL_CHAT_WIDGET, MENU_CELL_CHAT_WIDGET_FEEDBACK, MENU_CELL_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/notebook/browser/controller/chat/notebookChatContext'; +import { CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST, CTX_NOTEBOOK_CHAT_OUTER_FOCUS_POSITION, CTX_NOTEBOOK_CHAT_USER_DID_EDIT, MENU_CELL_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/notebook/browser/controller/chat/notebookChatContext'; import { ICellViewModel, INotebookEditor, INotebookEditorContribution, INotebookViewZone } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { registerNotebookContribution } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -423,10 +425,9 @@ export class NotebookChatController extends Disposable implements INotebookEdito ChatAgentLocation.Notebook, { telemetrySource: 'notebook-generate-cell', - inputMenuId: MENU_CELL_CHAT_INPUT, - widgetMenuId: MENU_CELL_CHAT_WIDGET, + inputMenuId: MenuId.ChatExecute, + widgetMenuId: MENU_INLINE_CHAT_WIDGET, statusMenuId: MENU_CELL_CHAT_WIDGET_STATUS, - feedbackMenuId: MENU_CELL_CHAT_WIDGET_FEEDBACK, rendererOptions: { renderTextEditsAsSummary: (uri) => { return isEqual(uri, this._widget?.parentEditor.getModel()?.uri) From cd93de1b0c15b2a9a2166c6f8863d47bee9d6357 Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Wed, 22 May 2024 14:13:13 -0700 Subject: [PATCH 328/357] Rename vsDeviceId (#213261) --- build/.moduleignore | 9 +++++++++ src/vs/base/node/id.ts | 2 +- src/vs/base/test/node/id.test.ts | 6 +++--- src/vs/code/electron-main/app.ts | 20 +++++++++---------- src/vs/code/node/cliProcessMain.ts | 6 +++--- .../node/sharedProcess/sharedProcessMain.ts | 2 +- .../standalone/browser/standaloneServices.ts | 2 +- .../electron-main/sharedProcess.ts | 4 ++-- .../sharedProcess/node/sharedProcess.ts | 2 +- .../telemetry/common/commonProperties.ts | 6 +++--- src/vs/platform/telemetry/common/telemetry.ts | 4 ++-- .../telemetry/common/telemetryService.ts | 4 ++-- .../telemetry/common/telemetryUtils.ts | 2 +- .../telemetry/electron-main/telemetryUtils.ts | 12 +++++------ .../platform/telemetry/node/telemetryUtils.ts | 14 ++++++------- src/vs/platform/window/common/window.ts | 2 +- .../electron-main/windowsMainService.ts | 4 ++-- src/vs/server/node/serverServices.ts | 8 ++++---- .../workbench/api/common/extHostTelemetry.ts | 2 +- .../api/test/browser/extHostTelemetry.test.ts | 2 +- .../electron-sandbox/environmentService.ts | 4 ++-- .../browser/webWorkerExtensionHost.ts | 2 +- .../common/extensionHostProtocol.ts | 2 +- .../extensions/common/remoteExtensionHost.ts | 2 +- .../localProcessExtensionHost.ts | 2 +- .../telemetry/browser/telemetryService.ts | 2 +- .../common/workbenchCommonProperties.ts | 4 ++-- .../electron-sandbox/telemetryService.ts | 4 ++-- .../test/node/commonProperties.test.ts | 6 +++--- .../workingCopyBackupService.test.ts | 2 +- 30 files changed, 76 insertions(+), 67 deletions(-) diff --git a/build/.moduleignore b/build/.moduleignore index e40224556c6..32fb3bd21c5 100644 --- a/build/.moduleignore +++ b/build/.moduleignore @@ -20,6 +20,15 @@ fsevents/test/** @vscode/spdlog/*.yml !@vscode/spdlog/build/Release/*.node +@vscode/deviceid/binding.gyp +@vscode/deviceid/build/** +@vscode/deviceid/deps/** +@vscode/deviceid/src/** +@vscode/deviceid/test/** +@vscode/deviceid/*.yml +!@vscode/deviceid/build/Release/*.node + + @vscode/sqlite3/binding.gyp @vscode/sqlite3/benchmark/** @vscode/sqlite3/cloudformation/** diff --git a/src/vs/base/node/id.ts b/src/vs/base/node/id.ts index 5f0fb74aefd..043731f666d 100644 --- a/src/vs/base/node/id.ts +++ b/src/vs/base/node/id.ts @@ -115,7 +115,7 @@ export async function getSqmMachineId(errorLogger: (error: any) => void): Promis return ''; } -export async function getVSDeviceId(errorLogger: (error: any) => void): Promise { +export async function getdevDeviceId(errorLogger: (error: any) => void): Promise { try { const deviceIdPackage = await import('@vscode/deviceid'); const id = await deviceIdPackage.getDeviceId(); diff --git a/src/vs/base/test/node/id.test.ts b/src/vs/base/test/node/id.test.ts index 3c733d92989..1a629134f06 100644 --- a/src/vs/base/test/node/id.test.ts +++ b/src/vs/base/test/node/id.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { getMachineId, getSqmMachineId, getVSDeviceId } from 'vs/base/node/id'; +import { getMachineId, getSqmMachineId, getdevDeviceId } from 'vs/base/node/id'; import { getMac } from 'vs/base/node/macAddress'; import { flakySuite } from 'vs/base/test/node/testUtils'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; @@ -26,9 +26,9 @@ flakySuite('ID', () => { assert.strictEqual(errors.length, 0); }); - test('getVSDeviceId', async function () { + test('getdevDeviceId', async function () { const errors = []; - const id = await getVSDeviceId(err => errors.push(err)); + const id = await getdevDeviceId(err => errors.push(err)); assert.ok(typeof id === 'string'); assert.strictEqual(errors.length, 0); }); diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index a56ef86fb2f..46193cdbe97 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -105,7 +105,7 @@ import { ExtensionsScannerService } from 'vs/platform/extensionManagement/node/e import { UserDataProfilesHandler } from 'vs/platform/userDataProfile/electron-main/userDataProfilesHandler'; import { ProfileStorageChangesListenerChannel } from 'vs/platform/userDataProfile/electron-main/userDataProfileStorageIpc'; import { Promises, RunOnceScheduler, runWhenGlobalIdle } from 'vs/base/common/async'; -import { resolveMachineId, resolveSqmId, resolveVSDeviceId } from 'vs/platform/telemetry/electron-main/telemetryUtils'; +import { resolveMachineId, resolveSqmId, resolvedevDeviceId } from 'vs/platform/telemetry/electron-main/telemetryUtils'; import { ExtensionsProfileScannerService } from 'vs/platform/extensionManagement/node/extensionsProfileScannerService'; import { LoggerChannel } from 'vs/platform/log/electron-main/logIpc'; import { ILoggerMainService } from 'vs/platform/log/electron-main/loggerService'; @@ -611,18 +611,18 @@ export class CodeApplication extends Disposable { // Resolve unique machine ID this.logService.trace('Resolving machine identifier...'); - const [machineId, sqmId, vsDeviceId] = await Promise.all([ + const [machineId, sqmId, devDeviceId] = await Promise.all([ resolveMachineId(this.stateService, this.logService), resolveSqmId(this.stateService, this.logService), - resolveVSDeviceId(this.stateService, this.logService) + resolvedevDeviceId(this.stateService, this.logService) ]); this.logService.trace(`Resolved machine identifier: ${machineId}`); // Shared process - const { sharedProcessReady, sharedProcessClient } = this.setupSharedProcess(machineId, sqmId, vsDeviceId); + const { sharedProcessReady, sharedProcessClient } = this.setupSharedProcess(machineId, sqmId, devDeviceId); // Services - const appInstantiationService = await this.initServices(machineId, sqmId, vsDeviceId, sharedProcessReady); + const appInstantiationService = await this.initServices(machineId, sqmId, devDeviceId, sharedProcessReady); // Auth Handler this._register(appInstantiationService.createInstance(ProxyAuthHandler)); @@ -987,8 +987,8 @@ export class CodeApplication extends Disposable { return false; } - private setupSharedProcess(machineId: string, sqmId: string, vsDeviceId: string): { sharedProcessReady: Promise; sharedProcessClient: Promise } { - const sharedProcess = this._register(this.mainInstantiationService.createInstance(SharedProcess, machineId, sqmId, vsDeviceId)); + private setupSharedProcess(machineId: string, sqmId: string, devDeviceId: string): { sharedProcessReady: Promise; sharedProcessClient: Promise } { + const sharedProcess = this._register(this.mainInstantiationService.createInstance(SharedProcess, machineId, sqmId, devDeviceId)); this._register(sharedProcess.onDidCrash(() => this.windowsMainService?.sendToFocused('vscode:reportSharedProcessCrash'))); @@ -1011,7 +1011,7 @@ export class CodeApplication extends Disposable { return { sharedProcessReady, sharedProcessClient }; } - private async initServices(machineId: string, sqmId: string, vsDeviceId: string, sharedProcessReady: Promise): Promise { + private async initServices(machineId: string, sqmId: string, devDeviceId: string, sharedProcessReady: Promise): Promise { const services = new ServiceCollection(); // Update @@ -1034,7 +1034,7 @@ export class CodeApplication extends Disposable { } // Windows - services.set(IWindowsMainService, new SyncDescriptor(WindowsMainService, [machineId, sqmId, vsDeviceId, this.userEnv], false)); + services.set(IWindowsMainService, new SyncDescriptor(WindowsMainService, [machineId, sqmId, devDeviceId, this.userEnv], false)); services.set(IAuxiliaryWindowsMainService, new SyncDescriptor(AuxiliaryWindowsMainService, undefined, false)); // Dialogs @@ -1114,7 +1114,7 @@ export class CodeApplication extends Disposable { const isInternal = isInternalTelemetry(this.productService, this.configurationService); const channel = getDelayedChannel(sharedProcessReady.then(client => client.getChannel('telemetryAppender'))); const appender = new TelemetryAppenderClient(channel); - const commonProperties = resolveCommonProperties(release(), hostname(), process.arch, this.productService.commit, this.productService.version, machineId, sqmId, vsDeviceId, isInternal); + const commonProperties = resolveCommonProperties(release(), hostname(), process.arch, this.productService.commit, this.productService.version, machineId, sqmId, devDeviceId, isInternal); const piiPaths = getPiiPathsFromEnvironment(this.environmentMainService); const config: ITelemetryServiceConfig = { appenders: [appender], commonProperties, piiPaths, sendErrorTelemetry: true }; diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index 62974272d38..2c1d7afc54c 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -57,7 +57,7 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { UserDataProfilesReadonlyService } from 'vs/platform/userDataProfile/node/userDataProfile'; -import { resolveMachineId, resolveSqmId, resolveVSDeviceId } from 'vs/platform/telemetry/node/telemetryUtils'; +import { resolveMachineId, resolveSqmId, resolvedevDeviceId } from 'vs/platform/telemetry/node/telemetryUtils'; import { ExtensionsProfileScannerService } from 'vs/platform/extensionManagement/node/extensionsProfileScannerService'; import { LogService } from 'vs/platform/log/common/logService'; import { LoggerService } from 'vs/platform/log/node/loggerService'; @@ -186,7 +186,7 @@ class CliMain extends Disposable { } } const sqmId = await resolveSqmId(stateService, logService); - const vsDeviceId = await resolveVSDeviceId(stateService, logService); + const devDeviceId = await resolvedevDeviceId(stateService, logService); // Initialize user data profiles after initializing the state userDataProfilesService.init(); @@ -222,7 +222,7 @@ class CliMain extends Disposable { const config: ITelemetryServiceConfig = { appenders, sendErrorTelemetry: false, - commonProperties: resolveCommonProperties(release(), hostname(), process.arch, productService.commit, productService.version, machineId, sqmId, vsDeviceId, isInternal), + commonProperties: resolveCommonProperties(release(), hostname(), process.arch, productService.commit, productService.version, machineId, sqmId, devDeviceId, isInternal), piiPaths: getPiiPathsFromEnvironment(environmentService) }; diff --git a/src/vs/code/node/sharedProcess/sharedProcessMain.ts b/src/vs/code/node/sharedProcess/sharedProcessMain.ts index 89d14e56747..f8e915491fc 100644 --- a/src/vs/code/node/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/node/sharedProcess/sharedProcessMain.ts @@ -307,7 +307,7 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter { telemetryService = new TelemetryService({ appenders, - commonProperties: resolveCommonProperties(release(), hostname(), process.arch, productService.commit, productService.version, this.configuration.machineId, this.configuration.sqmId, this.configuration.vsDeviceId, internalTelemetry), + commonProperties: resolveCommonProperties(release(), hostname(), process.arch, productService.commit, productService.version, this.configuration.machineId, this.configuration.sqmId, this.configuration.devDeviceId, internalTelemetry), sendErrorTelemetry: true, piiPaths: getPiiPathsFromEnvironment(environmentService), }, configurationService, productService); diff --git a/src/vs/editor/standalone/browser/standaloneServices.ts b/src/vs/editor/standalone/browser/standaloneServices.ts index fb624c2b126..5e15b129e0e 100644 --- a/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/src/vs/editor/standalone/browser/standaloneServices.ts @@ -792,7 +792,7 @@ class StandaloneTelemetryService implements ITelemetryService { readonly sessionId = 'someValue.sessionId'; readonly machineId = 'someValue.machineId'; readonly sqmId = 'someValue.sqmId'; - readonly vsDeviceId = 'someValue.vsDeviceId'; + readonly devDeviceId = 'someValue.devDeviceId'; readonly firstSessionDate = 'someValue.firstSessionDate'; readonly sendErrorTelemetry = false; setEnabled(): void { } diff --git a/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts b/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts index b94f362da38..5cb80e342cc 100644 --- a/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts +++ b/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts @@ -34,7 +34,7 @@ export class SharedProcess extends Disposable { constructor( private readonly machineId: string, private readonly sqmId: string, - private readonly vsDeviceId: string, + private readonly devDeviceId: string, @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, @@ -181,7 +181,7 @@ export class SharedProcess extends Disposable { return { machineId: this.machineId, sqmId: this.sqmId, - vsDeviceId: this.vsDeviceId, + devDeviceId: this.devDeviceId, codeCachePath: this.environmentMainService.codeCachePath, profiles: { home: this.userDataProfilesService.profilesHome, diff --git a/src/vs/platform/sharedProcess/node/sharedProcess.ts b/src/vs/platform/sharedProcess/node/sharedProcess.ts index df9d68f70b2..0c0e49c3a0b 100644 --- a/src/vs/platform/sharedProcess/node/sharedProcess.ts +++ b/src/vs/platform/sharedProcess/node/sharedProcess.ts @@ -15,7 +15,7 @@ export interface ISharedProcessConfiguration { readonly sqmId: string; - readonly vsDeviceId: string; + readonly devDeviceId: string; readonly codeCachePath: string | undefined; diff --git a/src/vs/platform/telemetry/common/commonProperties.ts b/src/vs/platform/telemetry/common/commonProperties.ts index d774c4f2f32..b649cb80775 100644 --- a/src/vs/platform/telemetry/common/commonProperties.ts +++ b/src/vs/platform/telemetry/common/commonProperties.ts @@ -24,7 +24,7 @@ export function resolveCommonProperties( version: string | undefined, machineId: string | undefined, sqmId: string | undefined, - vsDeviceId: string | undefined, + devDeviceId: string | undefined, isInternalTelemetry: boolean, product?: string ): ICommonProperties { @@ -34,8 +34,8 @@ export function resolveCommonProperties( result['common.machineId'] = machineId; // __GDPR__COMMON__ "common.sqmId" : { "endPoint": "SqmMachineId", "classification": "EndUserPseudonymizedInformation", "purpose": "BusinessInsight" } result['common.sqmId'] = sqmId; - // __GDPR__COMMON__ "common.vsDeviceId" : { "endPoint": "SqmMachineId", "classification": "EndUserPseudonymizedInformation", "purpose": "BusinessInsight" } - result['common.vsDeviceId'] = vsDeviceId; + // __GDPR__COMMON__ "common.devDeviceId" : { "endPoint": "SqmMachineId", "classification": "EndUserPseudonymizedInformation", "purpose": "BusinessInsight" } + result['common.devDeviceId'] = devDeviceId; // __GDPR__COMMON__ "sessionID" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } result['sessionID'] = generateUuid() + Date.now(); // __GDPR__COMMON__ "commitHash" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } diff --git a/src/vs/platform/telemetry/common/telemetry.ts b/src/vs/platform/telemetry/common/telemetry.ts index 98acd610cee..d6b4179b71f 100644 --- a/src/vs/platform/telemetry/common/telemetry.ts +++ b/src/vs/platform/telemetry/common/telemetry.ts @@ -23,7 +23,7 @@ export interface ITelemetryService { readonly sessionId: string; readonly machineId: string; readonly sqmId: string; - readonly vsDeviceId: string; + readonly devDeviceId: string; readonly firstSessionDate: string; readonly msftInternal?: boolean; @@ -74,7 +74,7 @@ export const firstSessionDateStorageKey = 'telemetry.firstSessionDate'; export const lastSessionDateStorageKey = 'telemetry.lastSessionDate'; export const machineIdKey = 'telemetry.machineId'; export const sqmIdKey = 'telemetry.sqmId'; -export const vsDeviceIdKey = 'telemetry.vsDeviceId'; +export const devDeviceIdKey = 'telemetry.devDeviceId'; // Configuration Keys export const TELEMETRY_SECTION_ID = 'telemetry'; diff --git a/src/vs/platform/telemetry/common/telemetryService.ts b/src/vs/platform/telemetry/common/telemetryService.ts index 131f6e1b61f..245bb41b5d8 100644 --- a/src/vs/platform/telemetry/common/telemetryService.ts +++ b/src/vs/platform/telemetry/common/telemetryService.ts @@ -34,7 +34,7 @@ export class TelemetryService implements ITelemetryService { readonly sessionId: string; readonly machineId: string; readonly sqmId: string; - readonly vsDeviceId: string; + readonly devDeviceId: string; readonly firstSessionDate: string; readonly msftInternal: boolean | undefined; @@ -59,7 +59,7 @@ export class TelemetryService implements ITelemetryService { this.sessionId = this._commonProperties['sessionID'] as string; this.machineId = this._commonProperties['common.machineId'] as string; this.sqmId = this._commonProperties['common.sqmId'] as string; - this.vsDeviceId = this._commonProperties['common.vsDeviceId'] as string; + this.devDeviceId = this._commonProperties['common.devDeviceId'] as string; this.firstSessionDate = this._commonProperties['common.firstSessionDate'] as string; this.msftInternal = this._commonProperties['common.msftInternal'] as boolean | undefined; diff --git a/src/vs/platform/telemetry/common/telemetryUtils.ts b/src/vs/platform/telemetry/common/telemetryUtils.ts index 45a8242943a..ec72e1a0829 100644 --- a/src/vs/platform/telemetry/common/telemetryUtils.ts +++ b/src/vs/platform/telemetry/common/telemetryUtils.ts @@ -30,7 +30,7 @@ export class NullTelemetryServiceShape implements ITelemetryService { readonly sessionId = 'someValue.sessionId'; readonly machineId = 'someValue.machineId'; readonly sqmId = 'someValue.sqmId'; - readonly vsDeviceId = 'someValue.vsDeviceId'; + readonly devDeviceId = 'someValue.devDeviceId'; readonly firstSessionDate = 'someValue.firstSessionDate'; readonly sendErrorTelemetry = false; publicLog() { } diff --git a/src/vs/platform/telemetry/electron-main/telemetryUtils.ts b/src/vs/platform/telemetry/electron-main/telemetryUtils.ts index 74916a938d1..95e993f8462 100644 --- a/src/vs/platform/telemetry/electron-main/telemetryUtils.ts +++ b/src/vs/platform/telemetry/electron-main/telemetryUtils.ts @@ -5,8 +5,8 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IStateService } from 'vs/platform/state/node/state'; -import { machineIdKey, sqmIdKey, vsDeviceIdKey } from 'vs/platform/telemetry/common/telemetry'; -import { resolveMachineId as resolveNodeMachineId, resolveSqmId as resolveNodeSqmId, resolveVSDeviceId as resolveNodeVSDeviceId } from 'vs/platform/telemetry/node/telemetryUtils'; +import { machineIdKey, sqmIdKey, devDeviceIdKey } from 'vs/platform/telemetry/common/telemetry'; +import { resolveMachineId as resolveNodeMachineId, resolveSqmId as resolveNodeSqmId, resolvedevDeviceId as resolveNodedevDeviceId } from 'vs/platform/telemetry/node/telemetryUtils'; export async function resolveMachineId(stateService: IStateService, logService: ILogService): Promise { // Call the node layers implementation to avoid code duplication @@ -21,8 +21,8 @@ export async function resolveSqmId(stateService: IStateService, logService: ILog return sqmId; } -export async function resolveVSDeviceId(stateService: IStateService, logService: ILogService): Promise { - const vsDeviceId = await resolveNodeVSDeviceId(stateService, logService); - stateService.setItem(vsDeviceIdKey, vsDeviceId); - return vsDeviceId; +export async function resolvedevDeviceId(stateService: IStateService, logService: ILogService): Promise { + const devDeviceId = await resolveNodedevDeviceId(stateService, logService); + stateService.setItem(devDeviceIdKey, devDeviceId); + return devDeviceId; } diff --git a/src/vs/platform/telemetry/node/telemetryUtils.ts b/src/vs/platform/telemetry/node/telemetryUtils.ts index 836b2925ca7..4f8fa8c8a5b 100644 --- a/src/vs/platform/telemetry/node/telemetryUtils.ts +++ b/src/vs/platform/telemetry/node/telemetryUtils.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { isMacintosh } from 'vs/base/common/platform'; -import { getMachineId, getSqmMachineId, getVSDeviceId } from 'vs/base/node/id'; +import { getMachineId, getSqmMachineId, getdevDeviceId } from 'vs/base/node/id'; import { ILogService } from 'vs/platform/log/common/log'; import { IStateReadService } from 'vs/platform/state/node/state'; -import { machineIdKey, sqmIdKey, vsDeviceIdKey } from 'vs/platform/telemetry/common/telemetry'; +import { machineIdKey, sqmIdKey, devDeviceIdKey } from 'vs/platform/telemetry/common/telemetry'; export async function resolveMachineId(stateService: IStateReadService, logService: ILogService): Promise { @@ -30,11 +30,11 @@ export async function resolveSqmId(stateService: IStateReadService, logService: return sqmId; } -export async function resolveVSDeviceId(stateService: IStateReadService, logService: ILogService): Promise { - let vsDeviceId = stateService.getItem(vsDeviceIdKey); - if (typeof vsDeviceId !== 'string') { - vsDeviceId = await getVSDeviceId(logService.error.bind(logService)); +export async function resolvedevDeviceId(stateService: IStateReadService, logService: ILogService): Promise { + let devDeviceId = stateService.getItem(devDeviceIdKey); + if (typeof devDeviceId !== 'string') { + devDeviceId = await getdevDeviceId(logService.error.bind(logService)); } - return vsDeviceId; + return devDeviceId; } diff --git a/src/vs/platform/window/common/window.ts b/src/vs/platform/window/common/window.ts index 007c84ba19d..ae20d9d7620 100644 --- a/src/vs/platform/window/common/window.ts +++ b/src/vs/platform/window/common/window.ts @@ -348,7 +348,7 @@ export interface INativeWindowConfiguration extends IWindowConfiguration, Native machineId: string; sqmId: string; - vsDeviceId: string; + devDeviceId: string; execPath: string; backupPath?: string; diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index cfb6fe56efa..a4cb0ca698b 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -211,7 +211,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic constructor( private readonly machineId: string, private readonly sqmId: string, - private readonly vsDeviceId: string, + private readonly devDeviceId: string, private readonly initialUserEnv: IProcessEnvironment, @ILogService private readonly logService: ILogService, @ILoggerMainService private readonly loggerService: ILoggerMainService, @@ -1410,7 +1410,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic machineId: this.machineId, sqmId: this.sqmId, - vsDeviceId: this.vsDeviceId, + devDeviceId: this.devDeviceId, windowId: -1, // Will be filled in by the window once loaded later diff --git a/src/vs/server/node/serverServices.ts b/src/vs/server/node/serverServices.ts index b4ffa2b38c5..46f2f004655 100644 --- a/src/vs/server/node/serverServices.ts +++ b/src/vs/server/node/serverServices.ts @@ -9,7 +9,7 @@ import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import * as path from 'vs/base/common/path'; import { IURITransformer } from 'vs/base/common/uriIpc'; -import { getMachineId, getSqmMachineId, getVSDeviceId } from 'vs/base/node/id'; +import { getMachineId, getSqmMachineId, getdevDeviceId } from 'vs/base/node/id'; import { Promises } from 'vs/base/node/pfs'; import { ClientConnectionEvent, IMessagePassingProtocol, IPCServer, StaticRouter } from 'vs/base/parts/ipc/common/ipc'; import { ProtocolConstants } from 'vs/base/parts/ipc/common/ipc.net'; @@ -132,12 +132,12 @@ export async function setupServerServices(connectionToken: ServerConnectionToken socketServer.registerChannel('userDataProfiles', new RemoteUserDataProfilesServiceChannel(userDataProfilesService, (ctx: RemoteAgentConnectionContext) => getUriTransformer(ctx.remoteAuthority))); // Initialize - const [, , machineId, sqmId, vsDeviceId] = await Promise.all([ + const [, , machineId, sqmId, devDeviceId] = await Promise.all([ configurationService.initialize(), userDataProfilesService.init(), getMachineId(logService.error.bind(logService)), getSqmMachineId(logService.error.bind(logService)), - getVSDeviceId(logService.error.bind(logService)) + getdevDeviceId(logService.error.bind(logService)) ]); const extensionHostStatusService = new ExtensionHostStatusService(); @@ -157,7 +157,7 @@ export async function setupServerServices(connectionToken: ServerConnectionToken const config: ITelemetryServiceConfig = { appenders: [oneDsAppender], - commonProperties: resolveCommonProperties(release(), hostname(), process.arch, productService.commit, productService.version + '-remote', machineId, sqmId, vsDeviceId, isInternal, 'remoteAgent'), + commonProperties: resolveCommonProperties(release(), hostname(), process.arch, productService.commit, productService.version + '-remote', machineId, sqmId, devDeviceId, isInternal, 'remoteAgent'), piiPaths: getPiiPathsFromEnvironment(environmentService) }; const initialTelemetryLevelArg = environmentService.args['telemetry-level']; diff --git a/src/vs/workbench/api/common/extHostTelemetry.ts b/src/vs/workbench/api/common/extHostTelemetry.ts index 99d2114de97..64a12869610 100644 --- a/src/vs/workbench/api/common/extHostTelemetry.ts +++ b/src/vs/workbench/api/common/extHostTelemetry.ts @@ -105,7 +105,7 @@ export class ExtHostTelemetry extends Disposable implements ExtHostTelemetryShap commonProperties['common.vscodemachineid'] = this.initData.telemetryInfo.machineId; commonProperties['common.vscodesessionid'] = this.initData.telemetryInfo.sessionId; commonProperties['common.sqmid'] = this.initData.telemetryInfo.sqmId; - commonProperties['common.vsdeviceid'] = this.initData.telemetryInfo.vsDeviceId; + commonProperties['common.devDeviceId'] = this.initData.telemetryInfo.devDeviceId; commonProperties['common.vscodeversion'] = this.initData.version; commonProperties['common.isnewappinstall'] = isNewAppInstall(this.initData.telemetryInfo.firstSessionDate); commonProperties['common.product'] = this.initData.environment.appHost; diff --git a/src/vs/workbench/api/test/browser/extHostTelemetry.test.ts b/src/vs/workbench/api/test/browser/extHostTelemetry.test.ts index c66ea784c31..97bfb308b9f 100644 --- a/src/vs/workbench/api/test/browser/extHostTelemetry.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTelemetry.test.ts @@ -45,7 +45,7 @@ suite('ExtHostTelemetry', function () { sessionId: 'test', machineId: 'test', sqmId: 'test', - vsDeviceId: 'test' + devDeviceId: 'test' }; const mockRemote = { diff --git a/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts b/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts index f8e0c3e498a..ce1d79f7751 100644 --- a/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts +++ b/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts @@ -39,7 +39,7 @@ export interface INativeWorkbenchEnvironmentService extends IBrowserWorkbenchEnv readonly os: IOSConfiguration; readonly machineId: string; readonly sqmId: string; - readonly vsDeviceId: string; + readonly devDeviceId: string; // --- Paths readonly execPath: string; @@ -65,7 +65,7 @@ export class NativeWorkbenchEnvironmentService extends AbstractNativeEnvironment get sqmId() { return this.configuration.sqmId; } @memoize - get vsDeviceId() { return this.configuration.vsDeviceId; } + get devDeviceId() { return this.configuration.devDeviceId; } @memoize get remoteAuthority() { return this.configuration.remoteAuthority; } diff --git a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts index 33b5adf244a..0c8d2e27317 100644 --- a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts +++ b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts @@ -310,7 +310,7 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost sessionId: this._telemetryService.sessionId, machineId: this._telemetryService.machineId, sqmId: this._telemetryService.sqmId, - vsDeviceId: this._telemetryService.vsDeviceId, + devDeviceId: this._telemetryService.devDeviceId, firstSessionDate: this._telemetryService.firstSessionDate, msftInternal: this._telemetryService.msftInternal }, diff --git a/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts b/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts index 57cc1052139..c44205e2453 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts @@ -41,7 +41,7 @@ export interface IExtensionHostInitData { readonly sessionId: string; readonly machineId: string; readonly sqmId: string; - readonly vsDeviceId: string; + readonly devDeviceId: string; readonly firstSessionDate: string; readonly msftInternal?: boolean; }; diff --git a/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts b/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts index 331257d1e4d..617256fb123 100644 --- a/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts +++ b/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts @@ -244,7 +244,7 @@ export class RemoteExtensionHost extends Disposable implements IExtensionHost { sessionId: this._telemetryService.sessionId, machineId: this._telemetryService.machineId, sqmId: this._telemetryService.sqmId, - vsDeviceId: this._telemetryService.vsDeviceId, + devDeviceId: this._telemetryService.devDeviceId, firstSessionDate: this._telemetryService.firstSessionDate, msftInternal: this._telemetryService.msftInternal }, diff --git a/src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts b/src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts index c96ce9ba199..0074c58d815 100644 --- a/src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts @@ -503,7 +503,7 @@ export class NativeLocalProcessExtensionHost implements IExtensionHost { sessionId: this._telemetryService.sessionId, machineId: this._telemetryService.machineId, sqmId: this._telemetryService.sqmId, - vsDeviceId: this._telemetryService.vsDeviceId, + devDeviceId: this._telemetryService.devDeviceId, firstSessionDate: this._telemetryService.firstSessionDate, msftInternal: this._telemetryService.msftInternal }, diff --git a/src/vs/workbench/services/telemetry/browser/telemetryService.ts b/src/vs/workbench/services/telemetry/browser/telemetryService.ts index 0feab911fda..29418eaed3a 100644 --- a/src/vs/workbench/services/telemetry/browser/telemetryService.ts +++ b/src/vs/workbench/services/telemetry/browser/telemetryService.ts @@ -29,7 +29,7 @@ export class TelemetryService extends Disposable implements ITelemetryService { get sessionId(): string { return this.impl.sessionId; } get machineId(): string { return this.impl.machineId; } get sqmId(): string { return this.impl.sqmId; } - get vsDeviceId(): string { return this.impl.vsDeviceId; } + get devDeviceId(): string { return this.impl.devDeviceId; } get firstSessionDate(): string { return this.impl.firstSessionDate; } get msftInternal(): boolean | undefined { return this.impl.msftInternal; } diff --git a/src/vs/workbench/services/telemetry/common/workbenchCommonProperties.ts b/src/vs/workbench/services/telemetry/common/workbenchCommonProperties.ts index 946adb270c8..6d8ec5b211f 100644 --- a/src/vs/workbench/services/telemetry/common/workbenchCommonProperties.ts +++ b/src/vs/workbench/services/telemetry/common/workbenchCommonProperties.ts @@ -17,12 +17,12 @@ export function resolveWorkbenchCommonProperties( version: string | undefined, machineId: string, sqmId: string, - vsDeviceId: string, + devDeviceId: string, isInternalTelemetry: boolean, process: INodeProcess, remoteAuthority?: string ): ICommonProperties { - const result = resolveCommonProperties(release, hostname, process.arch, commit, version, machineId, sqmId, vsDeviceId, isInternalTelemetry); + const result = resolveCommonProperties(release, hostname, process.arch, commit, version, machineId, sqmId, devDeviceId, isInternalTelemetry); const firstSessionDate = storageService.get(firstSessionDateStorageKey, StorageScope.APPLICATION)!; const lastSessionDate = storageService.get(lastSessionDateStorageKey, StorageScope.APPLICATION)!; diff --git a/src/vs/workbench/services/telemetry/electron-sandbox/telemetryService.ts b/src/vs/workbench/services/telemetry/electron-sandbox/telemetryService.ts index 52f333cd51d..384c53bba07 100644 --- a/src/vs/workbench/services/telemetry/electron-sandbox/telemetryService.ts +++ b/src/vs/workbench/services/telemetry/electron-sandbox/telemetryService.ts @@ -28,7 +28,7 @@ export class TelemetryService extends Disposable implements ITelemetryService { get sessionId(): string { return this.impl.sessionId; } get machineId(): string { return this.impl.machineId; } get sqmId(): string { return this.impl.sqmId; } - get vsDeviceId(): string { return this.impl.vsDeviceId; } + get devDeviceId(): string { return this.impl.devDeviceId; } get firstSessionDate(): string { return this.impl.firstSessionDate; } get msftInternal(): boolean | undefined { return this.impl.msftInternal; } @@ -46,7 +46,7 @@ export class TelemetryService extends Disposable implements ITelemetryService { const channel = sharedProcessService.getChannel('telemetryAppender'); const config: ITelemetryServiceConfig = { appenders: [new TelemetryAppenderClient(channel)], - commonProperties: resolveWorkbenchCommonProperties(storageService, environmentService.os.release, environmentService.os.hostname, productService.commit, productService.version, environmentService.machineId, environmentService.sqmId, environmentService.vsDeviceId, isInternal, process, environmentService.remoteAuthority), + commonProperties: resolveWorkbenchCommonProperties(storageService, environmentService.os.release, environmentService.os.hostname, productService.commit, productService.version, environmentService.machineId, environmentService.sqmId, environmentService.devDeviceId, isInternal, process, environmentService.remoteAuthority), piiPaths: getPiiPathsFromEnvironment(environmentService), sendErrorTelemetry: true }; diff --git a/src/vs/workbench/services/telemetry/test/node/commonProperties.test.ts b/src/vs/workbench/services/telemetry/test/node/commonProperties.test.ts index 8898cb332f7..e51736b4ee9 100644 --- a/src/vs/workbench/services/telemetry/test/node/commonProperties.test.ts +++ b/src/vs/workbench/services/telemetry/test/node/commonProperties.test.ts @@ -26,7 +26,7 @@ suite('Telemetry - common properties', function () { }); test('default', function () { - const props = resolveWorkbenchCommonProperties(testStorageService, release(), hostname(), commit, version, 'someMachineId', 'someSqmId', 'someVSDeviceId', false, process); + const props = resolveWorkbenchCommonProperties(testStorageService, release(), hostname(), commit, version, 'someMachineId', 'someSqmId', 'somedevDeviceId', false, process); assert.ok('commitHash' in props); assert.ok('sessionID' in props); assert.ok('timestamp' in props); @@ -50,14 +50,14 @@ suite('Telemetry - common properties', function () { testStorageService.store('telemetry.lastSessionDate', new Date().toUTCString(), StorageScope.APPLICATION, StorageTarget.MACHINE); - const props = resolveWorkbenchCommonProperties(testStorageService, release(), hostname(), commit, version, 'someMachineId', 'someSqmId', 'someVSDeviceId', false, process); + const props = resolveWorkbenchCommonProperties(testStorageService, release(), hostname(), commit, version, 'someMachineId', 'someSqmId', 'somedevDeviceId', false, process); assert.ok('common.lastSessionDate' in props); // conditional, see below assert.ok('common.isNewSession' in props); assert.strictEqual(props['common.isNewSession'], '0'); }); test('values chance on ask', async function () { - const props = resolveWorkbenchCommonProperties(testStorageService, release(), hostname(), commit, version, 'someMachineId', 'someSqmId', 'someVSDeviceId', false, process); + const props = resolveWorkbenchCommonProperties(testStorageService, release(), hostname(), commit, version, 'someMachineId', 'someSqmId', 'somedevDeviceId', false, process); let value1 = props['common.sequence']; let value2 = props['common.sequence']; assert.ok(value1 !== value2, 'seq'); diff --git a/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyBackupService.test.ts b/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyBackupService.test.ts index 6629be8043b..b4ee3100922 100644 --- a/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyBackupService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyBackupService.test.ts @@ -56,7 +56,7 @@ const TestNativeWindowConfiguration: INativeWindowConfiguration = { windowId: 0, machineId: 'testMachineId', sqmId: 'testSqmId', - vsDeviceId: 'testVSDeviceId', + devDeviceId: 'testdevDeviceId', logLevel: LogLevel.Error, loggers: { global: [], window: [] }, mainPid: 0, From 3996e0a00e4cd428d3e290a807aac1f9cf61769e Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 22 May 2024 23:37:25 +0200 Subject: [PATCH 329/357] polish profiles editor (#213262) --- .../browser/media/userDataProfilesEditor.css | 15 +-- .../browser/userDataProfilesEditor.ts | 91 +++++++++++++------ .../browser/userDataProfilesEditorModel.ts | 81 ++++------------- 3 files changed, 93 insertions(+), 94 deletions(-) diff --git a/src/vs/workbench/contrib/userDataProfile/browser/media/userDataProfilesEditor.css b/src/vs/workbench/contrib/userDataProfile/browser/media/userDataProfilesEditor.css index 5ed42a2fdca..c0674e54314 100644 --- a/src/vs/workbench/contrib/userDataProfile/browser/media/userDataProfilesEditor.css +++ b/src/vs/workbench/contrib/userDataProfile/browser/media/userDataProfilesEditor.css @@ -78,14 +78,17 @@ height: 28px; } -.profiles-editor .contents-container .profile-header .profile-actions-container .profile-button-container { - margin-left: 5px; - min-width: 120px; +.profiles-editor .contents-container .profile-header .profile-actions-container .actions-container { + gap: 4px; } -.profiles-editor .contents-container .profile-header .profile-actions-container .profile-button-container .monaco-button.error, -.profiles-editor .contents-container .profile-header .profile-actions-container .profile-button-container.error { - border: 1px solid var(--vscode-inputValidation-errorBorder, transparent); +.profiles-editor .contents-container .profile-header .profile-actions-container .actions-container .codicon { + font-size: 18px; +} + +.profiles-editor .contents-container .profile-header .profile-actions-container .profile-button-container { + margin-right: 5px; + min-width: 120px; } .profiles-editor .contents-container .profile-header .profile-actions-container .profile-button-container .monaco-button { diff --git a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts index c401df78f17..e14f9af5973 100644 --- a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts +++ b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/userDataProfilesEditor'; -import { $, addDisposableListener, append, Dimension, EventHelper, EventType, IDomPosition } from 'vs/base/browser/dom'; +import { $, addDisposableListener, append, Dimension, EventHelper, EventType, IDomPosition, trackFocus } from 'vs/base/browser/dom'; import { Action, IAction, Separator, SubmenuAction } from 'vs/base/common/actions'; import { Event } from 'vs/base/common/event'; import { ThemeIcon } from 'vs/base/common/themables'; @@ -32,7 +32,7 @@ import { IAsyncDataSource, IObjectTreeElement, ITreeNode, ITreeRenderer, ObjectT import { CancellationToken } from 'vs/base/common/cancellation'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { Disposable, DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; -import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; +import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; import { Checkbox } from 'vs/base/browser/ui/toggle/toggle'; import { DEFAULT_ICON, ICONS } from 'vs/workbench/services/userDataProfile/common/userDataProfileIcons'; import { WorkbenchIconSelectBox } from 'vs/workbench/services/userDataProfile/browser/iconSelectBox'; @@ -433,11 +433,9 @@ class ProfileWidget extends Disposable { append(title, $('span', undefined, localize('profile', "Profile: "))); this.profileTitle = append(title, $('span')); const actionsContainer = append(header, $('.profile-actions-container')); - this.actionbar = new ActionBar(actionsContainer, { - focusOnlyEnabledItems: true - }); - this.actionbar.setFocusable(false); this.buttonContainer = append(actionsContainer, $('.profile-button-container')); + this.actionbar = new ActionBar(actionsContainer); + this.actionbar.setFocusable(true); const body = append(parent, $('.profile-body')); @@ -452,13 +450,37 @@ class ProfileWidget extends Disposable { inputBoxStyles: defaultInputBoxStyles, ariaLabel: localize('profileName', "Profile Name"), placeholder: localize('profileName', "Profile Name"), + validationOptions: { + validation: (value) => { + if (!value) { + return { + content: localize('name required', "Profile name is required and must be a non-empty value."), + type: MessageType.ERROR + }; + } + const initialName = this._profileElement.value?.element instanceof UserDataProfileElement ? this._profileElement.value.element.profile.name : undefined; + if (initialName !== value && this.userDataProfilesService.profiles.some(p => p.name === value)) { + return { + content: localize('profileExists', "Profile with name {0} already exists.", value), + type: MessageType.ERROR + }; + } + return null; + } + } } )); this.nameInput.onDidChange(value => { - if (this._profileElement.value) { + if (this._profileElement.value && value) { this._profileElement.value.element.name = value; } }); + const focusTracker = this._register(trackFocus(this.nameInput.inputElement)); + this._register(focusTracker.onDidBlur(() => { + if (this._profileElement.value && !this.nameInput.value) { + this.nameInput.value = this._profileElement.value.element.name; + } + })); this.copyFromContainer = append(body, $('.profile-copy-from-container')); append(this.copyFromContainer, $('.profile-copy-from-label', undefined, localize('create from', "Copy from:"))); @@ -639,24 +661,29 @@ class ProfileWidget extends Disposable { } })); - const button = disposables.add(new Button(this.buttonContainer, { - supportIcons: true, - ...defaultButtonStyles - })); - button.label = profileElement.primaryAction.label; - button.enabled = profileElement.primaryAction.enabled; - disposables.add(button.onDidClick(() => this.editorProgressService.showWhile(profileElement.primaryAction.run()))); - disposables.add(profileElement.primaryAction.onDidChange((e) => { - if (!isUndefined(e.enabled)) { - button.enabled = profileElement.primaryAction.enabled; - } - })); - disposables.add(profileElement.onDidChange(e => { - if (e.message) { - button.setTitle(profileElement.message ?? profileElement.primaryAction.label); - button.element.classList.toggle('error', !!profileElement.message); - } - })); + if (profileElement.primaryAction) { + this.buttonContainer.classList.remove('hide'); + const button = disposables.add(new Button(this.buttonContainer, { + supportIcons: true, + ...defaultButtonStyles + })); + button.label = profileElement.primaryAction.label; + button.enabled = profileElement.primaryAction.enabled; + disposables.add(button.onDidClick(() => this.editorProgressService.showWhile(profileElement.primaryAction!.run()))); + disposables.add(profileElement.primaryAction.onDidChange((e) => { + if (!isUndefined(e.enabled)) { + button.enabled = profileElement.primaryAction!.enabled; + } + })); + disposables.add(profileElement.onDidChange(e => { + if (e.message) { + button.setTitle(profileElement.message ?? profileElement.primaryAction!.label); + button.element.classList.toggle('error', !!profileElement.message); + } + })); + } else { + this.buttonContainer.classList.add('hide'); + } this.actionbar.clear(); if (profileElement.secondaryActions.length > 0) { @@ -987,12 +1014,21 @@ export class UserDataProfilesEditorInput extends EditorInput { private readonly model: UserDataProfilesEditorModel; + private _dirty: boolean = false; + get dirty(): boolean { return this._dirty; } + set dirty(dirty: boolean) { + if (this._dirty !== dirty) { + this._dirty = dirty; + this._onDidChangeDirty.fire(); + } + } + constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); this.model = UserDataProfilesEditorModel.getInstance(this.instantiationService); - this._register(this.model.onDidChangeDirty(e => this._onDidChangeDirty.fire())); + this._register(this.model.onDidChange(e => this.dirty = this.model.profiles.some(profile => profile instanceof NewProfileElement))); } override get typeId(): string { return UserDataProfilesEditorInput.ID; } @@ -1004,10 +1040,11 @@ export class UserDataProfilesEditorInput extends EditorInput { } override isDirty(): boolean { - return this.model.isDirty(); + return this.dirty; } override async save(): Promise { + await this.model.saveNewProfile(); return this; } diff --git a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts index 0650951cf64..63872690376 100644 --- a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts +++ b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts @@ -25,13 +25,13 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; import { IFileService } from 'vs/platform/files/common/files'; import { generateUuid } from 'vs/base/common/uuid'; +import { RunOnceScheduler } from 'vs/base/common/async'; export type ChangeEvent = { readonly name?: boolean; readonly icon?: boolean; readonly flags?: boolean; readonly active?: boolean; - readonly dirty?: boolean; readonly message?: boolean; readonly copyFrom?: boolean; readonly copyFlags?: boolean; @@ -43,7 +43,6 @@ export interface IProfileElement { readonly icon?: string; readonly flags?: UseDefaultProfileFlags; readonly active?: boolean; - readonly dirty?: boolean; readonly message?: string; } @@ -57,7 +56,6 @@ export abstract class AbstractUserDataProfileElement extends Disposable { icon: string | undefined, flags: UseDefaultProfileFlags | undefined, isActive: boolean, - isDirty: boolean, @IUserDataProfilesService protected readonly userDataProfilesService: IUserDataProfilesService, @IInstantiationService protected readonly instantiationService: IInstantiationService, ) { @@ -66,15 +64,13 @@ export abstract class AbstractUserDataProfileElement extends Disposable { this._icon = icon; this._flags = flags; this._active = isActive; - this._dirty = isDirty; this._register(this.onDidChange(e => { - if (!e.dirty) { - this.dirty = this.hasUnsavedChanges(); - } if (!e.message) { this.validate(); } - this.primaryAction.enabled = !this.message && this.dirty; + if (this.primaryAction) { + this.primaryAction.enabled = !this.message; + } })); } @@ -114,15 +110,6 @@ export abstract class AbstractUserDataProfileElement extends Disposable { } } - private _dirty: boolean = false; - get dirty(): boolean { return this._dirty; } - set dirty(isDirty: boolean) { - if (this._dirty !== isDirty) { - this._dirty = isDirty; - this._onDidChange.fire({ dirty: true }); - } - } - private _message: string | undefined; get message(): string | undefined { return this._message; } set message(message: string | undefined) { @@ -147,10 +134,6 @@ export abstract class AbstractUserDataProfileElement extends Disposable { } validate(): void { - if (!this.dirty) { - this.message = undefined; - return; - } if (!this.name) { this.message = localize('profileNameRequired', "Profile name is required."); return; @@ -172,8 +155,6 @@ export abstract class AbstractUserDataProfileElement extends Disposable { return []; } - reset(): void { } - protected async getChildrenFromProfile(profile: IUserDataProfile, resourceType: ProfileResourceType): Promise { profile = this.getFlag(resourceType) ? this.userDataProfilesService.defaultProfile : profile; switch (resourceType) { @@ -195,16 +176,17 @@ export abstract class AbstractUserDataProfileElement extends Disposable { return ''; } - abstract readonly primaryAction: Action; + abstract readonly primaryAction?: Action; abstract readonly secondaryActions: IAction[]; - protected abstract hasUnsavedChanges(): boolean; } export class UserDataProfileElement extends AbstractUserDataProfileElement implements IProfileElement { get profile(): IUserDataProfile { return this._profile; } - readonly primaryAction = new Action('userDataProfile.save', localize('save', "Save"), undefined, this.dirty, () => this.save()); + readonly primaryAction = undefined; + + private readonly saveScheduler = this._register(new RunOnceScheduler(() => this.doSave(), 500)); constructor( private _profile: IUserDataProfile, @@ -219,7 +201,6 @@ export class UserDataProfileElement extends AbstractUserDataProfileElement imple _profile.icon, _profile.useDefaultFlags, userDataProfileService.currentProfile.id === _profile.id, - false, userDataProfilesService, instantiationService, ); @@ -233,9 +214,12 @@ export class UserDataProfileElement extends AbstractUserDataProfileElement imple this.flags = profile.useDefaultFlags; } })); + this._register(this.onDidChange(e => { + this.save(); + })); } - protected hasUnsavedChanges(): boolean { + private hasUnsavedChanges(): boolean { if (this.name !== this.profile.name) { return true; } @@ -248,8 +232,12 @@ export class UserDataProfileElement extends AbstractUserDataProfileElement imple return false; } - private async save(): Promise { - if (!this.dirty) { + save(): void { + this.saveScheduler.schedule(); + } + + private async doSave(): Promise { + if (!this.hasUnsavedChanges()) { return; } this.validate(); @@ -265,20 +253,12 @@ export class UserDataProfileElement extends AbstractUserDataProfileElement imple icon: this.icon, useDefaultFlags: this.profile.useDefaultFlags && !useDefaultFlags ? {} : useDefaultFlags }); - - this.dirty = false; } override async getChildren(resourceType: ProfileResourceType): Promise { return this.getChildrenFromProfile(this.profile, resourceType); } - override reset(): void { - this.name = this.profile.name; - this.icon = this.profile.icon; - this.flags = this.profile.useDefaultFlags; - } - protected override getInitialName(): string { return this.profile.name; } @@ -304,7 +284,6 @@ export class NewProfileElement extends AbstractUserDataProfileElement implements undefined, undefined, false, - true, userDataProfilesService, instantiationService, ); @@ -353,10 +332,6 @@ export class NewProfileElement extends AbstractUserDataProfileElement implements this.copyFlags = flags; } - protected hasUnsavedChanges(): boolean { - return true; - } - override async getChildren(resourceType: ProfileResourceType): Promise { if (!this.getCopyFlag(resourceType)) { return []; @@ -445,9 +420,6 @@ export class UserDataProfilesEditorModel extends EditorModel { private _onDidChange = this._register(new Emitter()); readonly onDidChange = this._onDidChange.event; - private _onDidChangeDirty = this._register(new Emitter()); - readonly onDidChangeDirty = this._onDidChangeDirty.event; - constructor( @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, @IUserDataProfileManagementService private readonly userDataProfileManagementService: IUserDataProfileManagementService, @@ -492,11 +464,6 @@ export class UserDataProfilesEditorModel extends EditorModel { profile, actions )); - disposables.add(profileElement.onDidChange(e => { - if (e.dirty) { - this._onDidChangeDirty.fire(this.isDirty()); - } - })); return [profileElement, disposables]; } @@ -508,7 +475,7 @@ export class UserDataProfilesEditorModel extends EditorModel { copyFrom, new Action('userDataProfile.create', localize('create', "Create & Apply"), undefined, true, () => this.saveNewProfile()), [ - new Action('userDataProfile.discard', localize('discard', "Discard"), ThemeIcon.asClassName(Codicon.trash), true, () => { + new Action('userDataProfile.discard', localize('discard', "Discard"), ThemeIcon.asClassName(Codicon.close), true, () => { this.removeNewProfile(); this._onDidChange.fire(undefined); }) @@ -520,16 +487,8 @@ export class UserDataProfilesEditorModel extends EditorModel { return this.newProfileElement; } - isDirty(): boolean { - return this._profiles.some(([p]) => p.dirty); - } - revert(): void { this.removeNewProfile(); - for (const [profile] of this._profiles) { - profile.reset(); - } - this._onDidChangeDirty.fire(false); this._onDidChange.fire(undefined); } @@ -543,7 +502,7 @@ export class UserDataProfilesEditorModel extends EditorModel { } } - private async saveNewProfile(): Promise { + async saveNewProfile(): Promise { if (!this.newProfileElement) { return; } From 37e1242f153248c7cf7c95903d461fc9d7cc4ffe Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Wed, 22 May 2024 14:47:06 -0700 Subject: [PATCH 330/357] refactor: clean up chat context types (#213264) --- .../browser/actions/chatContextActions.ts | 93 ++++++++++++++++--- .../contrib/chat/browser/chatVariables.ts | 6 +- .../contrib/chat/common/chatAgents.ts | 1 + 3 files changed, 85 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index d55100c335e..75d9f745760 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -23,6 +23,7 @@ import { ChatContextAttachments } from 'vs/workbench/contrib/chat/browser/contri import { SelectAndInsertFileAction } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables'; import { ChatAgentLocation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { CONTEXT_CHAT_LOCATION, CONTEXT_IN_CHAT_INPUT } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { IChatRequestVariableEntry } from 'vs/workbench/contrib/chat/common/chatModel'; import { ChatRequestAgentPart } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import { AnythingQuickAccessProvider } from 'vs/workbench/contrib/search/browser/anythingQuickAccess'; @@ -31,6 +32,36 @@ export function registerChatContextActions() { registerAction2(AttachContextAction); } +export type IChatContextQuickPickItem = IFileQuickPickItem | IDynamicVariableQuickPickItem | IStaticVariableQuickPickItem; + +export interface IFileQuickPickItem extends IQuickPickItem { + id: string; + name: string; + value: URI; + + resource: URI; + isDynamic: true; +} + +export interface IDynamicVariableQuickPickItem extends IQuickPickItem { + id: string; + name?: string; + value: unknown; + icon?: ThemeIcon; + + command?: Command; + isDynamic: true; +} + +export interface IStaticVariableQuickPickItem extends IQuickPickItem { + id: string; + name: string; + value: unknown; + icon?: ThemeIcon; + + isDynamic?: false; +} + class AttachContextAction extends Action2 { static readonly ID = 'workbench.action.chat.attachContext'; @@ -61,20 +92,43 @@ class AttachContextAction extends Action2 { }); } - private async _attachContext(widget: IChatWidget, commandService: ICommandService, ...picks: any[]) { - const toAttach = []; + private async _attachContext(widget: IChatWidget, commandService: ICommandService, ...picks: IChatContextQuickPickItem[]) { + const toAttach: IChatRequestVariableEntry[] = []; for (const pick of picks) { - if (pick && typeof pick === 'object' && 'command' in pick) { + if (pick && typeof pick === 'object' && 'command' in pick && pick.command) { + // Dynamic variable with a followup command const selection = await commandService.executeCommand(pick.command.id, ...(pick.command.arguments ?? [])); - if (selection) { - const qualifiedName = `${typeof pick.value === 'string' && pick.value.startsWith('#') ? pick.value.slice(1) : ''}${selection}`; - - toAttach.push({ ...pick, isDynamic: pick.isDynamic, value: pick.value, name: qualifiedName, fullName: `$(${pick.icon.id}) ${selection}` }); + if (!selection) { + // User made no selection, skip this variable + continue; } + toAttach.push({ + ...pick, + isDynamic: pick.isDynamic, + value: pick.value, + name: `${typeof pick.value === 'string' && pick.value.startsWith('#') ? pick.value.slice(1) : ''}${selection}`, + // Apply the original icon with the new name + fullName: `${pick.icon ? `$(${pick.icon.id}) ` : ''}${selection}` + }); } else if (pick && typeof pick === 'object' && 'resource' in pick) { - toAttach.push({ ...pick, value: pick.resource, name: pick.label, id: pick.resource.toString(), isDynamic: true }); + // #file variable + toAttach.push({ + ...pick, + id: pick.resource.toString(), + value: pick.resource, + name: pick.label, + isDynamic: true + }); } else { - toAttach.push({ ...pick, fullName: pick.label, name: 'name' in pick && typeof pick.name === 'string' ? pick.name : pick.label, icon: 'icon' in pick && ThemeIcon.isThemeIcon(pick.icon) ? pick.icon : undefined }); + // All other dynamic variables and static variables + toAttach.push({ + ...pick, + id: pick.id, + value: pick.value, + fullName: pick.label, + name: 'name' in pick && typeof pick.name === 'string' ? pick.name : pick.label, + icon: 'icon' in pick && ThemeIcon.isThemeIcon(pick.icon) ? pick.icon : undefined + }); } } @@ -95,10 +149,15 @@ class AttachContextAction extends Action2 { const usedAgent = widget.parsedInput.parts.find(p => p instanceof ChatRequestAgentPart); const slowSupported = usedAgent ? usedAgent.agent.metadata.supportsSlowVariables : true; - const quickPickItems: (QuickPickItem & { isDynamic?: boolean; name?: string; icon?: ThemeIcon; command?: Command; value?: unknown })[] = []; + const quickPickItems: (IChatContextQuickPickItem | QuickPickItem)[] = []; for (const variable of chatVariablesService.getVariables()) { if (variable.fullName && (!variable.isSlow || slowSupported)) { - quickPickItems.push({ label: `${variable.icon ? `$(${variable.icon.id}) ` : ''}${variable.fullName}`, name: variable.name, id: variable.id, icon: variable.icon }); + quickPickItems.push({ + label: `${variable.icon ? `$(${variable.icon.id}) ` : ''}${variable.fullName}`, + name: variable.name, + id: variable.id, + icon: variable.icon + }); } } @@ -108,7 +167,15 @@ class AttachContextAction extends Action2 { const completions = await chatAgentService.getAgentCompletionItems(agentPart.agent.id, '', CancellationToken.None); for (const variable of completions) { if (variable.fullName) { - quickPickItems.push({ label: `${variable.icon ? `$(${variable.icon.id}) ` : ''}${variable.fullName}`, id: variable.id, command: variable.command, icon: variable.icon, value: variable.value, isDynamic: true }); + quickPickItems.push({ + label: `${variable.icon ? `$(${variable.icon.id}) ` : ''}${variable.fullName}`, + id: variable.id, + command: variable.command, + icon: variable.icon, + value: variable.value, + isDynamic: true, + name: variable.name + }); } } } @@ -123,7 +190,7 @@ class AttachContextAction extends Action2 { enabledProviderPrefixes: [AnythingQuickAccessProvider.PREFIX], placeholder: localize('chatContext.attach.placeholder', 'Search attachments'), providerOptions: { - handleAccept: (item: IQuickPickItem) => { + handleAccept: (item: IChatContextQuickPickItem) => { this._attachContext(widget, commandService, item); }, additionPicks: quickPickItems, diff --git a/src/vs/workbench/contrib/chat/browser/chatVariables.ts b/src/vs/workbench/contrib/chat/browser/chatVariables.ts index 4dad704cf2c..acde7f02757 100644 --- a/src/vs/workbench/contrib/chat/browser/chatVariables.ts +++ b/src/vs/workbench/contrib/chat/browser/chatVariables.ts @@ -63,6 +63,7 @@ export class ChatVariablesService implements IChatVariablesService { } }); + const resolvedAttachedContext: IChatRequestVariableEntry[] = []; attachedContextVariables ?.forEach((attachment, i) => { const data = this._resolver.get(attachment.name?.toLowerCase()); @@ -77,11 +78,11 @@ export class ChatVariablesService implements IChatVariablesService { }; jobs.push(data.resolver(prompt.text, '', model, variableProgressCallback, token).then(value => { if (value) { - resolvedVariables[i] = { id: data.data.id, modelDescription: data.data.modelDescription, name: attachment.name, range: attachment.range, value, references }; + resolvedAttachedContext[i] = { id: data.data.id, modelDescription: data.data.modelDescription, name: attachment.name, range: attachment.range, value, references }; } }).catch(onUnexpectedExternalError)); } else if (attachment.isDynamic) { - resolvedVariables[i] = { id: attachment.id, name: attachment.name, value: attachment.value }; + resolvedAttachedContext[i] = { id: attachment.id, name: attachment.name, value: attachment.value }; } }); @@ -91,6 +92,7 @@ export class ChatVariablesService implements IChatVariablesService { // "reverse", high index first so that replacement is simple resolvedVariables.sort((a, b) => b.range!.start - a.range!.start); + resolvedVariables.push(...resolvedAttachedContext); return { variables: resolvedVariables, diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index 3068f8bbdf7..43cf0f8e680 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -148,6 +148,7 @@ interface IChatAgentEntry { export interface IChatAgentCompletionItem { id: string; + name?: string; fullName?: string; icon?: ThemeIcon; value: unknown; From 637b1f42e7f8e664cb9e884b2600c0c33cd6d2fc Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Wed, 22 May 2024 15:13:35 -0700 Subject: [PATCH 331/357] fix: prevent attaching the same chat context twice (#213266) --- .../browser/actions/chatContextActions.ts | 32 +++++++++++++------ .../browser/contrib/chatContextAttachments.ts | 4 +++ 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index 75d9f745760..d63dd9fe8b7 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -38,28 +38,28 @@ export interface IFileQuickPickItem extends IQuickPickItem { id: string; name: string; value: URI; + isDynamic: true; resource: URI; - isDynamic: true; } export interface IDynamicVariableQuickPickItem extends IQuickPickItem { id: string; name?: string; value: unknown; - icon?: ThemeIcon; - - command?: Command; isDynamic: true; + + icon?: ThemeIcon; + command?: Command; } export interface IStaticVariableQuickPickItem extends IQuickPickItem { id: string; name: string; value: unknown; - icon?: ThemeIcon; - isDynamic?: false; + + icon?: ThemeIcon; } class AttachContextAction extends Action2 { @@ -92,6 +92,10 @@ class AttachContextAction extends Action2 { }); } + private _getFileContextId(item: { resource: URI }) { + return item.resource.toString(); + } + private async _attachContext(widget: IChatWidget, commandService: ICommandService, ...picks: IChatContextQuickPickItem[]) { const toAttach: IChatRequestVariableEntry[] = []; for (const pick of picks) { @@ -114,7 +118,7 @@ class AttachContextAction extends Action2 { // #file variable toAttach.push({ ...pick, - id: pick.resource.toString(), + id: this._getFileContextId(pick), value: pick.resource, name: pick.label, isDynamic: true @@ -195,10 +199,20 @@ class AttachContextAction extends Action2 { }, additionPicks: quickPickItems, includeSymbols: false, - filter: (item) => { + filter: (item: IChatContextQuickPickItem) => { + // Avoid attaching the same context twice + const attachedContext = widget.getContrib(ChatContextAttachments.ID)?.getContext() ?? new Set(); + if (item && typeof item === 'object' && 'resource' in item && URI.isUri(item.resource)) { - return [Schemas.file, Schemas.vscodeRemote].includes(item.resource.scheme); + return [Schemas.file, Schemas.vscodeRemote].includes(item.resource.scheme) + && !attachedContext.has(this._getFileContextId({ resource: item.resource })); // Hack because Typescript doesn't narrow this type correctly } + + if (!('command' in item)) { + return !attachedContext.has(item.id); + } + + // Don't filter out dynamic variables which show secondary data (temporary) return true; } } diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatContextAttachments.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatContextAttachments.ts index c70b4d7e5a5..ff0a4455486 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatContextAttachments.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatContextAttachments.ts @@ -42,6 +42,10 @@ export class ChatContextAttachments extends Disposable implements IChatWidgetCon this.widget.setContext(true, ...s); } + getContext() { + return new Set([...this._attachedContext.values()].map((v) => v.id)); + } + setContext(overwrite: boolean, ...attachments: IChatRequestVariableEntry[]) { if (overwrite) { this._attachedContext.clear(); From 96a37971fe50ab7ec7efb9d0a42d4f829370c681 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 22 May 2024 15:39:10 -0700 Subject: [PATCH 332/357] Bump distro (#213268) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5c1633fce10..a7a658eab13 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.90.0", - "distro": "95d725e64e7e797849db840c27108f9f2e85a678", + "distro": "91d9dc4bb762ea314baeaf02cd6ede0d8148b04e", "author": { "name": "Microsoft Corporation" }, From 21df88c9b47c8d4cee2a202f4f0b7dd46bbc4140 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 23 May 2024 01:26:51 +0200 Subject: [PATCH 333/357] more polish (#213267) * more polish * refactor actions * fix hover --- .../browser/media/userDataProfilesEditor.css | 4 +- .../browser/userDataProfilesEditor.ts | 71 ++++++++++--------- .../browser/userDataProfilesEditorModel.ts | 57 ++++++++++----- 3 files changed, 81 insertions(+), 51 deletions(-) diff --git a/src/vs/workbench/contrib/userDataProfile/browser/media/userDataProfilesEditor.css b/src/vs/workbench/contrib/userDataProfile/browser/media/userDataProfilesEditor.css index c0674e54314..0233dfd5309 100644 --- a/src/vs/workbench/contrib/userDataProfile/browser/media/userDataProfilesEditor.css +++ b/src/vs/workbench/contrib/userDataProfile/browser/media/userDataProfilesEditor.css @@ -6,7 +6,7 @@ .profiles-editor { height: 100%; overflow: hidden; - max-width: 1200px; + max-width: 1000px; margin: 20px auto 0px auto; } @@ -159,7 +159,7 @@ } .profiles-editor .contents-container .profile-tree-item-container.new-profile-resource-type-container > .profile-resource-type-label-container { - width: 250px; + width: 150px; } .profiles-editor .contents-container .profile-tree-item-container.new-profile-resource-type-container > .profile-select-container { diff --git a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts index e14f9af5973..4c294d03ac2 100644 --- a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts +++ b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts @@ -53,11 +53,12 @@ import { API_OPEN_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/ed import { SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { DEFAULT_LABELS_CONTAINER, IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; -import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; -import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { AbstractUserDataProfileElement, IProfileElement, NewProfileElement, UserDataProfileElement, UserDataProfilesEditorModel } from 'vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel'; import { Codicon } from 'vs/base/common/codicons'; +import { WorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; +import { createInstantHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; export const profilesSashBorder = registerColor('profiles.sashBorder', { dark: PANEL_BORDER, light: PANEL_BORDER, hcDark: PANEL_BORDER, hcLight: PANEL_BORDER }, localize('profilesSashBorder', "The color of the Profiles editor splitview sash border.")); @@ -80,6 +81,7 @@ export class UserDataProfilesEditor extends EditorPane implements IUserDataProfi @IStorageService storageService: IStorageService, @IUserDataProfileManagementService private readonly userDataProfileManagementService: IUserDataProfileManagementService, @IQuickInputService private readonly quickInputService: IQuickInputService, + @IDialogService private readonly dialogService: IDialogService, @IFileDialogService private readonly fileDialogService: IFileDialogService, @IContextMenuService private readonly contextMenuService: IContextMenuService, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -115,7 +117,7 @@ export class UserDataProfilesEditor extends EditorPane implements IUserDataProfi onDidChange: Event.None, element: sidebarView, minimumSize: 175, - maximumSize: 300, + maximumSize: 350, layout: (width, _, height) => { sidebarView.style.width = `${width}px`; if (height && this.profilesTree) { @@ -123,7 +125,7 @@ export class UserDataProfilesEditor extends EditorPane implements IUserDataProfi this.profilesTree.layout(height - 38, width); } } - }, 250, undefined, true); + }, 300, undefined, true); this.splitView.addView({ onDidChange: Event.None, element: contentsView, @@ -192,7 +194,7 @@ export class UserDataProfilesEditor extends EditorPane implements IUserDataProfi if (this.templates.length) { actions.push(new SubmenuAction('from.template', localize('from template', "From Template"), this.templates.map(template => new Action(`template:${template.url}`, template.name, undefined, true, async () => { - this.model?.createNewProfile(URI.parse(template.url)); + this.createNewProfile(URI.parse(template.url)); })))); actions.push(new Separator()); } @@ -206,12 +208,7 @@ export class UserDataProfilesEditor extends EditorPane implements IUserDataProfi ...defaultButtonStyles })); button.label = `$(add) ${localize('newProfile', "New Profile")}`; - this._register(button.onDidClick(e => { - if (this.model) { - const element = this.model.createNewProfile(); - this.updateProfilesTree(element); - } - })); + this._register(button.onDidClick(e => this.createNewProfile())); } private registerListeners(): void { @@ -227,7 +224,7 @@ export class UserDataProfilesEditor extends EditorPane implements IUserDataProfi if (e.element instanceof AbstractUserDataProfileElement) { this.contextMenuService.showContextMenu({ getAnchor: () => e.anchor, - getActions: () => e.element instanceof AbstractUserDataProfileElement ? e.element.secondaryActions : [], + getActions: () => e.element instanceof AbstractUserDataProfileElement ? e.element.contextMenuActions.slice(0) : [], getActionsContext: () => e.element }); } @@ -263,13 +260,28 @@ export class UserDataProfilesEditor extends EditorPane implements IUserDataProfi } const url = selectedItem.label === quickPick.value ? URI.parse(quickPick.value) : await this.getProfileUriFromFileSystem(); if (url) { - this.model?.createNewProfile(url); + this.createNewProfile(url); } })); disposables.add(quickPick.onDidHide(() => disposables.dispose())); quickPick.show(); } + private async createNewProfile(copyFrom?: URI | IUserDataProfile): Promise { + if (this.model?.profiles.some(p => p instanceof NewProfileElement)) { + const result = await this.dialogService.confirm({ + type: 'info', + message: localize('new profile exists', "A new profile is already being created. Do you want to discard it and create a new one?"), + primaryButton: localize('discard', "Discard & Create"), + cancelButton: localize('cancel', "Cancel") + }); + if (!result.confirmed) { + return; + } + this.model.revert(); + } + this.model?.createNewProfile(copyFrom); + } private async getProfileUriFromFileSystem(): Promise { const profileLocation = await this.fileDialogService.showOpenDialog({ @@ -398,7 +410,7 @@ class ProfileTreeElementRenderer implements ITreeRenderer 0) { - this.actionbar.push(profileElement.secondaryActions.filter(a => !(a instanceof Separator)), { icon: true, label: false }); - } + this.toolbar.setActions(profileElement.titleActions[0].slice(0), profileElement.titleActions[1].slice(0)); this.nameInput.focus(); if (profileElement instanceof NewProfileElement) { @@ -806,7 +819,6 @@ interface INewProfileResourceTemplateData extends IProfileResourceTemplateData { readonly label: HTMLElement; readonly selectContainer: HTMLElement; readonly selectBox: SelectBox; - readonly description: HTMLElement; } interface IProfileResourceChildTreeItemTemplateData extends IProfileResourceTemplateData { @@ -907,12 +919,11 @@ class NewProfileResourceTreeRenderer extends AbstractProfileResourceTreeRenderer const container = append(parent, $('.profile-tree-item-container.new-profile-resource-type-container')); const labelContainer = append(container, $('.profile-resource-type-label-container')); const label = append(labelContainer, $('span.profile-resource-type-label')); - const description = append(labelContainer, $('span.profile-resource-type-description', undefined, localize('use default', "Use Default Profile"))); const selectBox = this._register(this.instantiationService.createInstance(SelectBox, [ - { text: localize('copy', "Copy"), description: localize('copy from selected', "Copy from selected") }, { text: localize('empty', "Empty") }, - { text: localize('exclude', "Exclude"), description: localize('use default', "Use Default Profile") } + { text: localize('copy', "Copy") }, + { text: localize('default', "Use Default Profile") } ], 0, this.contextViewService, @@ -924,7 +935,7 @@ class NewProfileResourceTreeRenderer extends AbstractProfileResourceTreeRenderer const selectContainer = append(container, $('.profile-select-container')); selectBox.render(selectContainer); - return { label, selectContainer, selectBox, description, disposables, elementDisposables: disposables.add(new DisposableStore()) }; + return { label, selectContainer, selectBox, disposables, elementDisposables: disposables.add(new DisposableStore()) }; } renderElement({ element: profileResourceTreeElement }: ITreeNode, index: number, templateData: INewProfileResourceTemplateData, height: number | undefined): void { @@ -937,16 +948,10 @@ class NewProfileResourceTreeRenderer extends AbstractProfileResourceTreeRenderer throw new Error('NewProfileResourceTreeRenderer can only profile resoyrce types'); } templateData.label.textContent = this.getResourceTypeTitle(element); - templateData.description.classList.toggle('hide', !root.getFlag(element)); - templateData.selectBox.select(root.getCopyFlag(element) ? 0 : root.getFlag(element) ? 2 : 1); + templateData.selectBox.select(root.getCopyFlag(element) ? 1 : root.getFlag(element) ? 2 : 0); templateData.elementDisposables.add(templateData.selectBox.onDidSelect(option => { root.setFlag(element, option.index === 2); - root.setCopyFlag(element, option.index === 0); - })); - templateData.elementDisposables.add(root.onDidChange(e => { - if (e.flags) { - templateData.description.classList.toggle('hide', !root.getFlag(element)); - } + root.setCopyFlag(element, option.index === 1); })); } } diff --git a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts index 63872690376..bde549ecefa 100644 --- a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts +++ b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts @@ -177,7 +177,8 @@ export abstract class AbstractUserDataProfileElement extends Disposable { } abstract readonly primaryAction?: Action; - abstract readonly secondaryActions: IAction[]; + abstract readonly titleActions: [IAction[], IAction[]]; + abstract readonly contextMenuActions: IAction[]; } export class UserDataProfileElement extends AbstractUserDataProfileElement implements IProfileElement { @@ -190,7 +191,8 @@ export class UserDataProfileElement extends AbstractUserDataProfileElement imple constructor( private _profile: IUserDataProfile, - readonly secondaryActions: IAction[], + readonly titleActions: [IAction[], IAction[]], + readonly contextMenuActions: IAction[], @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, @IUserDataProfileManagementService private readonly userDataProfileManagementService: IUserDataProfileManagementService, @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService, @@ -273,7 +275,8 @@ export class NewProfileElement extends AbstractUserDataProfileElement implements name: string, copyFrom: URI | IUserDataProfile | undefined, readonly primaryAction: Action, - readonly secondaryActions: Action[], + readonly titleActions: [IAction[], IAction[]], + readonly contextMenuActions: Action[], @IFileService private readonly fileService: IFileService, @IUserDataProfileImportExportService private readonly userDataProfileImportExportService: IUserDataProfileImportExportService, @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService, @@ -421,6 +424,7 @@ export class UserDataProfilesEditorModel extends EditorModel { readonly onDidChange = this._onDidChange.event; constructor( + @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, @IUserDataProfileManagementService private readonly userDataProfileManagementService: IUserDataProfileManagementService, @IUserDataProfileImportExportService private readonly userDataProfileImportExportService: IUserDataProfileImportExportService, @@ -453,16 +457,37 @@ export class UserDataProfilesEditorModel extends EditorModel { private createProfileElement(profile: IUserDataProfile): [UserDataProfileElement, DisposableStore] { const disposables = new DisposableStore(); - const actions: IAction[] = []; - actions.push(new Action('userDataProfile.copyFromProfile', localize('copyFromProfile', "Save As..."), ThemeIcon.asClassName(Codicon.copy), true, () => this.createNewProfile(profile))); - actions.push(new Action('userDataProfile.export', localize('export', "Export..."), ThemeIcon.asClassName(Codicon.export), true, () => this.exportProfile(profile))); + + const activateAction = disposables.add(new Action('userDataProfile.activate', localize('active', "Activate"), ThemeIcon.asClassName(Codicon.check), true, () => this.userDataProfileManagementService.switchProfile(profile))); + activateAction.checked = this.userDataProfileService.currentProfile.id === profile.id; + disposables.add(this.userDataProfileService.onDidChangeCurrentProfile(() => activateAction.checked = this.userDataProfileService.currentProfile.id === profile.id)); + const copyFromProfileAction = disposables.add(new Action('userDataProfile.copyFromProfile', localize('copyFromProfile', "Save As..."), ThemeIcon.asClassName(Codicon.copy), true, () => this.createNewProfile(profile))); + const exportAction = disposables.add(new Action('userDataProfile.export', localize('export', "Export..."), ThemeIcon.asClassName(Codicon.export), true, () => this.exportProfile(profile))); + const deleteAction = disposables.add(new Action('userDataProfile.delete', localize('delete', "Delete"), ThemeIcon.asClassName(Codicon.trash), true, () => this.removeProfile(profile))); + + const titlePrimaryActions: IAction[] = []; + titlePrimaryActions.push(activateAction); + titlePrimaryActions.push(exportAction); if (!profile.isDefault) { - actions.push(new Separator()); - actions.push(new Action('userDataProfile.delete', localize('delete', "Delete"), ThemeIcon.asClassName(Codicon.trash), true, () => this.removeProfile(profile))); + titlePrimaryActions.push(deleteAction); + } + + const titleSecondaryActions: IAction[] = []; + titleSecondaryActions.push(copyFromProfileAction); + + const secondaryActions: IAction[] = []; + secondaryActions.push(activateAction); + secondaryActions.push(new Separator()); + secondaryActions.push(copyFromProfileAction); + secondaryActions.push(exportAction); + if (!profile.isDefault) { + secondaryActions.push(new Separator()); + secondaryActions.push(deleteAction); } const profileElement = disposables.add(this.instantiationService.createInstance(UserDataProfileElement, profile, - actions + [titlePrimaryActions, titleSecondaryActions], + secondaryActions, )); return [profileElement, disposables]; } @@ -470,16 +495,16 @@ export class UserDataProfilesEditorModel extends EditorModel { createNewProfile(copyFrom?: URI | IUserDataProfile): IProfileElement { if (!this.newProfileElement) { const disposables = new DisposableStore(); + const discardAction = disposables.add(new Action('userDataProfile.discard', localize('discard', "Discard"), ThemeIcon.asClassName(Codicon.close), true, () => { + this.removeNewProfile(); + this._onDidChange.fire(undefined); + })); this.newProfileElement = disposables.add(this.instantiationService.createInstance(NewProfileElement, localize('untitled', "Untitled"), copyFrom, - new Action('userDataProfile.create', localize('create', "Create & Apply"), undefined, true, () => this.saveNewProfile()), - [ - new Action('userDataProfile.discard', localize('discard', "Discard"), ThemeIcon.asClassName(Codicon.close), true, () => { - this.removeNewProfile(); - this._onDidChange.fire(undefined); - }) - ] + disposables.add(new Action('userDataProfile.create', localize('create', "Create & Apply"), undefined, true, () => this.saveNewProfile())), + [[discardAction], []], + [discardAction], )); this._profiles.push([this.newProfileElement, disposables]); this._onDidChange.fire(this.newProfileElement); From 79d8e9809edd9531c063ae116a20a60b017ba345 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Wed, 22 May 2024 18:02:36 -0700 Subject: [PATCH 334/357] Prevent using disposed of timers (#213271) * Prevent using disposed of timers If a timer has been disposed of, warn and don't schedule the callback. This is needed as otherwise the timer very likely ends up being leaked * Throw errors instead --- src/vs/base/common/async.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/vs/base/common/async.ts b/src/vs/base/common/async.ts index 02e911ebd32..bf6342078da 100644 --- a/src/vs/base/common/async.ts +++ b/src/vs/base/common/async.ts @@ -880,6 +880,7 @@ export class ResourceQueue implements IDisposable { export class TimeoutTimer implements IDisposable { private _token: any; + private _isDisposed = false; constructor(); constructor(runner: () => void, timeout: number); @@ -893,6 +894,7 @@ export class TimeoutTimer implements IDisposable { dispose(): void { this.cancel(); + this._isDisposed = true; } cancel(): void { @@ -903,6 +905,10 @@ export class TimeoutTimer implements IDisposable { } cancelAndSet(runner: () => void, timeout: number): void { + if (this._isDisposed) { + throw new BugIndicatingError(`Calling 'cancelAndSet' on a disposed TimeoutTimer`); + } + this.cancel(); this._token = setTimeout(() => { this._token = -1; @@ -911,6 +917,10 @@ export class TimeoutTimer implements IDisposable { } setIfNotSet(runner: () => void, timeout: number): void { + if (this._isDisposed) { + throw new BugIndicatingError(`Calling 'setIfNotSet' on a disposed TimeoutTimer`); + } + if (this._token !== -1) { // timer is already set return; @@ -925,6 +935,7 @@ export class TimeoutTimer implements IDisposable { export class IntervalTimer implements IDisposable { private disposable: IDisposable | undefined = undefined; + private isDisposed = false; cancel(): void { this.disposable?.dispose(); @@ -932,6 +943,10 @@ export class IntervalTimer implements IDisposable { } cancelAndSet(runner: () => void, interval: number, context = globalThis): void { + if (this.isDisposed) { + throw new BugIndicatingError(`Calling 'cancelAndSet' on a disposed IntervalTimer`); + } + this.cancel(); const handle = context.setInterval(() => { runner(); @@ -945,6 +960,7 @@ export class IntervalTimer implements IDisposable { dispose(): void { this.cancel(); + this.isDisposed = true; } } From 17f711b9bc64a11f4ec48956ef30df9a886d57c7 Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Wed, 22 May 2024 18:56:10 -0700 Subject: [PATCH 335/357] Fix #213206. Migrate off IInlineChatService (#213263) * Fix #213206. Migrate off IInlineChatService * fix tests --- .../cellDiagnosticEditorContrib.ts | 10 +++---- .../contrib/notebookCellDiagnostics.test.ts | 27 ++++++++++++++++--- .../test/browser/testNotebookEditor.ts | 3 --- 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnosticEditorContrib.ts b/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnosticEditorContrib.ts index a04c894f931..79608fb88b4 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnosticEditorContrib.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnosticEditorContrib.ts @@ -7,7 +7,6 @@ import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle' import { IMarkerData, IMarkerService } from 'vs/platform/markers/common/markers'; import { IRange } from 'vs/editor/common/core/range'; import { ICellExecutionError, ICellExecutionStateChangedEvent, IExecutionStateChangedEvent, INotebookExecutionStateService, NotebookExecutionType } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; -import { IInlineChatService } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { CellKind, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookEditor, INotebookEditorContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; @@ -16,6 +15,7 @@ import { Iterable } from 'vs/base/common/iterator'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { URI } from 'vs/base/common/uri'; import { Event } from 'vs/base/common/event'; +import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; type CellDiagnostic = { cellUri: URI; @@ -35,14 +35,14 @@ export class CellDiagnostics extends Disposable implements INotebookEditorContri private readonly notebookEditor: INotebookEditor, @INotebookExecutionStateService private readonly notebookExecutionStateService: INotebookExecutionStateService, @IMarkerService private readonly markerService: IMarkerService, - @IInlineChatService private readonly inlineChatService: IInlineChatService, + @IChatAgentService private readonly chatAgentService: IChatAgentService, @IConfigurationService private readonly configurationService: IConfigurationService ) { super(); this.updateEnabled(); - this._register(inlineChatService.onDidChangeProviders(() => this.updateEnabled())); + this._register(chatAgentService.onDidChangeAgents(() => this.updateEnabled())); this._register(configurationService.onDidChangeConfiguration((e) => { if (e.affectsConfiguration(NotebookSetting.cellFailureDiagnostics)) { this.updateEnabled(); @@ -52,10 +52,10 @@ export class CellDiagnostics extends Disposable implements INotebookEditorContri private updateEnabled() { const settingEnabled = this.configurationService.getValue(NotebookSetting.cellFailureDiagnostics); - if (this.enabled && (!settingEnabled || Iterable.isEmpty(this.inlineChatService.getAllProvider()))) { + if (this.enabled && (!settingEnabled || Iterable.isEmpty(this.chatAgentService.getAgents()))) { this.enabled = false; this.clearAll(); - } else if (!this.enabled && settingEnabled && !Iterable.isEmpty(this.inlineChatService.getAllProvider())) { + } else if (!this.enabled && settingEnabled && !Iterable.isEmpty(this.chatAgentService.getAgents())) { this.enabled = true; if (!this.listening) { this.listening = true; diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookCellDiagnostics.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookCellDiagnostics.test.ts index 677f7f5b850..ef1c965adb2 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookCellDiagnostics.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookCellDiagnostics.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { Emitter } from 'vs/base/common/event'; +import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ResourceMap } from 'vs/base/common/map'; import { waitForState } from 'vs/base/common/observable'; @@ -15,12 +15,13 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { IMarkerData, IMarkerService } from 'vs/platform/markers/common/markers'; -import { IInlineChatService, IInlineChatSessionProvider } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { ChatAgentLocation, IChatAgent, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { CellDiagnostics } from 'vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnosticEditorContrib'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { CellKind, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ICellExecutionStateChangedEvent, IExecutionStateChangedEvent, INotebookCellExecution, INotebookExecutionStateService, NotebookExecutionType } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { setupInstantiationService, TestNotebookExecutionStateService, withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; +import { nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; suite('notebookCellDiagnostics', () => { @@ -64,8 +65,26 @@ suite('notebookCellDiagnostics', () => { testExecutionService = new TestExecutionService(); instantiationService.stub(INotebookExecutionStateService, testExecutionService); - const chatProviders = instantiationService.get(IInlineChatService); - disposables.add(chatProviders.addProvider({} as IInlineChatSessionProvider)); + const agentData = { + extensionId: nullExtensionDescription.identifier, + extensionDisplayName: '', + extensionPublisherId: '', + name: 'testEditorAgent', + isDefault: true, + locations: [ChatAgentLocation.Editor], + metadata: {}, + slashCommands: [] + }; + const chatAgentService = new class extends mock() { + override getAgents(): IChatAgentData[] { + return [{ + id: 'testEditorAgent', + ...agentData + }]; + } + override onDidChangeAgents: Event = Event.None; + }; + instantiationService.stub(IChatAgentService, chatAgentService); markerService = new class extends mock() { override markers: ResourceMap = new ResourceMap(); diff --git a/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts index 677afcd4ab8..6db24ac2b10 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts @@ -65,8 +65,6 @@ import { EditorFontLigatures, EditorFontVariations } from 'vs/editor/common/conf import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { mainWindow } from 'vs/base/browser/window'; import { TestCodeEditorService } from 'vs/editor/test/browser/editorTestServices'; -import { IInlineChatService } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; -import { InlineChatServiceImpl } from 'vs/workbench/contrib/inlineChat/common/inlineChatServiceImpl'; import { INotebookCellOutlineProviderFactory, NotebookCellOutlineProviderFactory } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProviderFactory'; import { ILanguageDetectionService } from 'vs/workbench/services/languageDetection/common/languageDetectionWorkerService'; @@ -201,7 +199,6 @@ export function setupInstantiationService(disposables: DisposableStore) { instantiationService.stub(IKeybindingService, new MockKeybindingService()); instantiationService.stub(INotebookCellStatusBarService, disposables.add(new NotebookCellStatusBarService())); instantiationService.stub(ICodeEditorService, disposables.add(new TestCodeEditorService(testThemeService))); - instantiationService.stub(IInlineChatService, instantiationService.createInstance(InlineChatServiceImpl)); instantiationService.stub(INotebookCellOutlineProviderFactory, instantiationService.createInstance(NotebookCellOutlineProviderFactory)); instantiationService.stub(ILanguageDetectionService, new class MockLanguageDetectionService implements ILanguageDetectionService { From cf2db6c1e68d8444e7826e44b280b96908d0c505 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 22 May 2024 19:07:45 -0700 Subject: [PATCH 336/357] Fix leaking uponSanitizeElement hook (#213277) --- src/vs/base/browser/markdownRenderer.ts | 30 +++++++++++++++++-------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts index d5d424d9134..8f4e098b005 100644 --- a/src/vs/base/browser/markdownRenderer.ts +++ b/src/vs/base/browser/markdownRenderer.ts @@ -17,7 +17,7 @@ import { markdownEscapeEscapedIcons } from 'vs/base/common/iconLabels'; import { defaultGenerator } from 'vs/base/common/idGenerator'; import { KeyCode } from 'vs/base/common/keyCodes'; import { Lazy } from 'vs/base/common/lazy'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { marked } from 'vs/base/common/marked/marked'; import { parse } from 'vs/base/common/marshalling'; import { FileAccess, Schemas } from 'vs/base/common/network'; @@ -383,7 +383,8 @@ function sanitizeRenderedMarkdown( renderedMarkdown: string, ): TrustedHTML { const { config, allowedSchemes } = getSanitizerOptions(options); - dompurify.addHook('uponSanitizeAttribute', (element, e) => { + const store = new DisposableStore(); + store.add(addDompurifyHook('uponSanitizeAttribute', (element, e) => { if (e.attrName === 'style' || e.attrName === 'class') { if (element.tagName === 'SPAN') { if (e.attrName === 'style') { @@ -403,9 +404,9 @@ function sanitizeRenderedMarkdown( } e.keepAttr = false; } - }); + })); - dompurify.addHook('uponSanitizeElement', (element, e) => { + store.add(addDompurifyHook('uponSanitizeElement', (element, e) => { if (e.tagName === 'input') { if (element.attributes.getNamedItem('type')?.value === 'checkbox') { element.setAttribute('disabled', ''); @@ -413,16 +414,14 @@ function sanitizeRenderedMarkdown( element.parentElement?.removeChild(element); } } - }); + })); - - const hook = DOM.hookDomPurifyHrefAndSrcSanitizer(allowedSchemes); + store.add(DOM.hookDomPurifyHrefAndSrcSanitizer(allowedSchemes)); try { return dompurify.sanitize(renderedMarkdown, { ...config, RETURN_TRUSTED_TYPE: true }); } finally { - dompurify.removeHook('uponSanitizeAttribute'); - hook.dispose(); + store.dispose(); } } @@ -876,3 +875,16 @@ function completeTable(tokens: marked.Token[]): marked.Token[] | undefined { return undefined; } + +function addDompurifyHook( + hook: 'uponSanitizeElement', + cb: (currentNode: Element, data: dompurify.SanitizeElementHookEvent, config: dompurify.Config) => void, +): IDisposable; +function addDompurifyHook( + hook: 'uponSanitizeAttribute', + cb: (currentNode: Element, data: dompurify.SanitizeAttributeHookEvent, config: dompurify.Config) => void, +): IDisposable; +function addDompurifyHook(hook: 'uponSanitizeElement' | 'uponSanitizeAttribute', cb: any): IDisposable { + dompurify.addHook(hook, cb); + return toDisposable(() => dompurify.removeHook(hook)); +} From 3cae619a1139d38d9e05b7dfc94c5326e20230ec Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 22 May 2024 19:11:55 -0700 Subject: [PATCH 337/357] Tweak agent hover (#213278) "View in Marketplace" seems to be confusing people --- src/vs/workbench/contrib/chat/browser/chatAgentHover.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts b/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts index 7e738c0ccfe..9163fd24fdb 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts @@ -121,7 +121,7 @@ export function getChatAgentHoverOptions(getAgent: () => IChatAgentData | undefi actions: [ { commandId: showExtensionsWithIdsCommandId, - label: localize('marketplaceLabel', "View in Marketplace"), + label: localize('viewExtensionLabel', "View Extension"), run: () => { const agent = getAgent(); if (agent) { From 878cb471aa63428a5e7d37efd48695f7dfea31b1 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 22 May 2024 19:42:18 -0700 Subject: [PATCH 338/357] Fix fillInIncompleteTokens for ordered lists (#213280) --- src/vs/base/browser/markdownRenderer.ts | 10 ++- .../test/browser/markdownRenderer.test.ts | 61 +++++++++++++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts index 8f4e098b005..5a7f7fe9aa5 100644 --- a/src/vs/base/browser/markdownRenderer.ts +++ b/src/vs/base/browser/markdownRenderer.ts @@ -715,8 +715,14 @@ function completeListItemPattern(list: marked.Tokens.List): marked.Tokens.List | const previousListItemsText = mergeRawTokenText(list.items.slice(0, -1)); - // Grabbing the `- ` off the list item because I can't find a better way to do this - const newListItemText = lastListItem.raw.slice(0, 2) + + // Grabbing the `- ` or `1. ` off the list item because I can't find a better way to do this + const lastListItemLead = lastListItem.raw.match(/^(\s*(-|\d+\.) +)/)?.[0]; + if (!lastListItemLead) { + // Is badly formatted + return; + } + + const newListItemText = lastListItemLead + mergeRawTokenText(lastListItem.tokens.slice(0, -1)) + newToken.raw; diff --git a/src/vs/base/test/browser/markdownRenderer.test.ts b/src/vs/base/test/browser/markdownRenderer.test.ts index 3a42aba4374..f9099c653e9 100644 --- a/src/vs/base/test/browser/markdownRenderer.test.ts +++ b/src/vs/base/test/browser/markdownRenderer.test.ts @@ -606,6 +606,15 @@ const y = 2; const completeTokens = marked.lexer(text + delimiter); assert.deepStrictEqual(newTokens, completeTokens); }); + + test(`incomplete ${name} in numbered list`, () => { + const text = `1. list item one\n2. list item two and ${delimiter}text`; + const tokens = marked.lexer(text); + const newTokens = fillInIncompleteTokens(tokens); + + const completeTokens = marked.lexer(text + delimiter); + assert.deepStrictEqual(newTokens, completeTokens); + }); } suite('list', () => { @@ -646,6 +655,18 @@ const y = 2; assert.deepStrictEqual(newTokens, tokens); }); + test('ordered list with subitems', () => { + const list = `1. hello + - sub item +2. text + newline for some reason +`; + const tokens = marked.lexer(list); + const newTokens = fillInIncompleteTokens(tokens); + + assert.deepStrictEqual(newTokens, tokens); + }); + test('list with stuff', () => { const list = `- list item one \`codespan\` **bold** [link](http://microsoft.com) more text`; const tokens = marked.lexer(list); @@ -674,6 +695,36 @@ const y = 2; assert.deepStrictEqual(newTokens, completeTokens); }); + test('ordered list with incomplete link target', () => { + const incomplete = `1. list item one +2. item two [link](`; + const tokens = marked.lexer(incomplete); + const newTokens = fillInIncompleteTokens(tokens); + + const completeTokens = marked.lexer(incomplete + ')'); + assert.deepStrictEqual(newTokens, completeTokens); + }); + + test('ordered list with extra whitespace', () => { + const incomplete = `1. list item one +2. item two [link](`; + const tokens = marked.lexer(incomplete); + const newTokens = fillInIncompleteTokens(tokens); + + const completeTokens = marked.lexer(incomplete + ')'); + assert.deepStrictEqual(newTokens, completeTokens); + }); + + test('list with extra whitespace', () => { + const incomplete = `- list item one +- item two [link](`; + const tokens = marked.lexer(incomplete); + const newTokens = fillInIncompleteTokens(tokens); + + const completeTokens = marked.lexer(incomplete + ')'); + assert.deepStrictEqual(newTokens, completeTokens); + }); + test('list with incomplete link with other stuff', () => { const incomplete = `- list item one - item two [\`link`; @@ -683,6 +734,16 @@ const y = 2; const completeTokens = marked.lexer(incomplete + '\`](https://microsoft.com)'); assert.deepStrictEqual(newTokens, completeTokens); }); + + test('ordered list with incomplete link with other stuff', () => { + const incomplete = `1. list item one +1. item two [\`link`; + const tokens = marked.lexer(incomplete); + const newTokens = fillInIncompleteTokens(tokens); + + const completeTokens = marked.lexer(incomplete + '\`](https://microsoft.com)'); + assert.deepStrictEqual(newTokens, completeTokens); + }); }); suite('codespan', () => { From 2b277fcb563fe182444b8c2cf3ac41f95daa4b74 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Wed, 22 May 2024 21:22:34 -0700 Subject: [PATCH 339/357] fix: apply query filter to `additionPicks` (#213281) --- .../search/browser/anythingQuickAccess.ts | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts index bdf461d00de..c9a116bdbfe 100644 --- a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts @@ -345,7 +345,26 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider(); if (options.additionPicks) { - picks.push(...options.additionPicks); + for (const pick of options.additionPicks) { + if (pick.type === 'separator') { + picks.push(pick); + continue; + } + if (!query.original) { + pick.highlights = undefined; + picks.push(pick); + continue; + } + const { score, labelMatch, descriptionMatch } = scoreItemFuzzy(pick, query, true, quickPickItemScorerAccessor, this.pickState.scorerCache); + if (!score) { + continue; + } + pick.highlights = { + label: labelMatch, + description: descriptionMatch + }; + picks.push(pick); + } } if (this.pickState.isQuickNavigating) { if (picks.length > 0) { From ba6fb3b36e9bab8c31ab1c03a53dd3cd2d491f4b Mon Sep 17 00:00:00 2001 From: Alexandru Dima Date: Thu, 23 May 2024 08:50:44 +0200 Subject: [PATCH 340/357] Fix StableEditorBottomScrollState (#212970) --- src/vs/editor/browser/stableEditorScroll.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/browser/stableEditorScroll.ts b/src/vs/editor/browser/stableEditorScroll.ts index fe02be80430..986e18ac6c0 100644 --- a/src/vs/editor/browser/stableEditorScroll.ts +++ b/src/vs/editor/browser/stableEditorScroll.ts @@ -5,6 +5,7 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { Position } from 'vs/editor/common/core/position'; +import { ScrollType } from 'vs/editor/common/editorCommon'; export class StableEditorScrollState { @@ -59,7 +60,7 @@ export class StableEditorScrollState { } const offset = editor.getTopForLineNumber(currentCursorPosition.lineNumber) - editor.getTopForLineNumber(this._cursorPosition.lineNumber); - editor.setScrollTop(editor.getScrollTop() + offset); + editor.setScrollTop(editor.getScrollTop() + offset, ScrollType.Immediate); } } @@ -78,7 +79,7 @@ export class StableEditorBottomScrollState { if (visibleRanges.length > 0) { visiblePosition = visibleRanges.at(-1)!.getEndPosition(); const visiblePositionScrollBottom = editor.getBottomForLineNumber(visiblePosition.lineNumber); - visiblePositionScrollDelta = (editor.getScrollTop() + editor.getLayoutInfo().height) - visiblePositionScrollBottom; + visiblePositionScrollDelta = visiblePositionScrollBottom - editor.getScrollTop(); } return new StableEditorBottomScrollState(editor.getScrollTop(), editor.getContentHeight(), visiblePosition, visiblePositionScrollDelta); } @@ -99,7 +100,7 @@ export class StableEditorBottomScrollState { if (this._visiblePosition) { const visiblePositionScrollBottom = editor.getBottomForLineNumber(this._visiblePosition.lineNumber); - editor.setScrollTop(visiblePositionScrollBottom - (this._visiblePositionScrollDelta + editor.getLayoutInfo().height)); + editor.setScrollTop(visiblePositionScrollBottom - this._visiblePositionScrollDelta, ScrollType.Immediate); } } } From 869828c0978004ea059416b1ff4c596bbe64ddfe Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 23 May 2024 09:49:30 +0200 Subject: [PATCH 341/357] False positive "saving too long" dialog hindering saving a file (fix microsoft/vscode-internalbacklog#4943) (#213290) --- .../workingCopyBackupTracker.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupTracker.ts b/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupTracker.ts index 000801f1726..eb982284efd 100644 --- a/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupTracker.ts +++ b/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupTracker.ts @@ -350,7 +350,14 @@ export class NativeWorkingCopyBackupTracker extends WorkingCopyBackupTracker imp if (result !== false) { await Promises.settled(workingCopies.map(workingCopy => workingCopy.isModified() ? workingCopy.save(saveOptions) : Promise.resolve(true))); } - }, localize('saveBeforeShutdown', "Saving editors with unsaved changes is taking a bit longer...")); + }, + localize('saveBeforeShutdown', "Saving editors with unsaved changes is taking a bit longer..."), + undefined, + // Do not pick `Dialog` as location for reporting progress if it is likely + // that the save operation will itself open a dialog for asking for the + // location to save to for untitled or scratchpad working copies. + // https://github.com/microsoft/vscode-internalbacklog/issues/4943 + workingCopies.some(workingCopy => workingCopy.capabilities & WorkingCopyCapabilities.Untitled || workingCopy.capabilities & WorkingCopyCapabilities.Scratchpad) ? ProgressLocation.Window : ProgressLocation.Dialog); } private doRevertAllBeforeShutdown(modifiedWorkingCopies: IWorkingCopy[]): Promise { @@ -439,13 +446,13 @@ export class NativeWorkingCopyBackupTracker extends WorkingCopyBackupTracker imp }, localize('discardBackupsBeforeShutdown', "Discarding backups is taking a bit longer...")); } - private withProgressAndCancellation(promiseFactory: (token: CancellationToken) => Promise, title: string, detail?: string): Promise { + private withProgressAndCancellation(promiseFactory: (token: CancellationToken) => Promise, title: string, detail?: string, location = ProgressLocation.Dialog): Promise { const cts = new CancellationTokenSource(); return this.progressService.withProgress({ - location: ProgressLocation.Dialog, // use a dialog to prevent the user from making any more changes now (https://github.com/microsoft/vscode/issues/122774) - cancellable: true, // allow to cancel (https://github.com/microsoft/vscode/issues/112278) - delay: 800, // delay so that it only appears when operation takes a long time + location, // by default use a dialog to prevent the user from making any more changes now (https://github.com/microsoft/vscode/issues/122774) + cancellable: true, // allow to cancel (https://github.com/microsoft/vscode/issues/112278) + delay: 800, // delay so that it only appears when operation takes a long time title, detail }, () => raceCancellation(promiseFactory(cts.token), cts.token), () => cts.dispose(true)); From 082435c697d7e012bb89ec240680348dc4b4c0f1 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 23 May 2024 10:08:58 +0200 Subject: [PATCH 342/357] Remove tree view show hover (#213212) --- .../api/browser/viewsExtensionPoint.ts | 61 +------------------ .../workbench/browser/actions/listCommands.ts | 25 ++++++-- .../workbench/browser/parts/views/treeView.ts | 5 -- .../views/browser/treeViewsService.ts | 12 ---- .../services/views/common/treeViewsService.ts | 34 ----------- 5 files changed, 22 insertions(+), 115 deletions(-) delete mode 100644 src/vs/workbench/services/views/browser/treeViewsService.ts delete mode 100644 src/vs/workbench/services/views/common/treeViewsService.ts diff --git a/src/vs/workbench/api/browser/viewsExtensionPoint.ts b/src/vs/workbench/api/browser/viewsExtensionPoint.ts index 73ca041e3db..b28bd289ca6 100644 --- a/src/vs/workbench/api/browser/viewsExtensionPoint.ts +++ b/src/vs/workbench/api/browser/viewsExtensionPoint.ts @@ -11,14 +11,14 @@ import { localize } from 'vs/nls'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ExtensionIdentifier, ExtensionIdentifierSet, IExtensionDescription, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Registry } from 'vs/platform/registry/common/platform'; import { ThemeIcon } from 'vs/base/common/themables'; import { Extensions as ViewletExtensions, PaneCompositeRegistry } from 'vs/workbench/browser/panecomposite'; -import { CustomTreeView, RawCustomTreeViewContextKey, TreeViewPane } from 'vs/workbench/browser/parts/views/treeView'; +import { CustomTreeView, TreeViewPane } from 'vs/workbench/browser/parts/views/treeView'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; -import { Extensions as ViewContainerExtensions, ICustomViewDescriptor, IViewContainersRegistry, IViewDescriptor, IViewsRegistry, ResolvableTreeItem, ViewContainer, ViewContainerLocation } from 'vs/workbench/common/views'; +import { Extensions as ViewContainerExtensions, ICustomViewDescriptor, IViewContainersRegistry, IViewDescriptor, IViewsRegistry, ViewContainer, ViewContainerLocation } from 'vs/workbench/common/views'; import { VIEWLET_ID as DEBUG } from 'vs/workbench/contrib/debug/common/debug'; import { VIEWLET_ID as EXPLORER } from 'vs/workbench/contrib/files/common/files'; import { VIEWLET_ID as REMOTE } from 'vs/workbench/contrib/remote/browser/remoteExplorer'; @@ -26,14 +26,6 @@ import { VIEWLET_ID as SCM } from 'vs/workbench/contrib/scm/common/scm'; import { WebviewViewPane } from 'vs/workbench/contrib/webviewView/browser/webviewViewPane'; import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionMessageCollector, ExtensionsRegistry, IExtensionPoint, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { IListService, WorkbenchListFocusContextKey } from 'vs/platform/list/browser/listService'; -import { IHoverService } from 'vs/platform/hover/browser/hover'; -import { CancellationTokenSource } from 'vs/base/common/cancellation'; -import { AsyncDataTree } from 'vs/base/browser/ui/tree/asyncDataTree'; -import { ITreeViewsService } from 'vs/workbench/services/views/browser/treeViewsService'; -import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; import { ILogService } from 'vs/platform/log/common/log'; import { IExtensionFeatureTableRenderer, IRenderedData, ITableData, IRowData, IExtensionFeaturesRegistry, Extensions as ExtensionFeaturesRegistryExtensions } from 'vs/workbench/services/extensionManagement/common/extensionFeatures'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -291,53 +283,6 @@ class ViewsExtensionHandler implements IWorkbenchContribution { this.viewsRegistry = Registry.as(ViewContainerExtensions.ViewsRegistry); this.handleAndRegisterCustomViewContainers(); this.handleAndRegisterCustomViews(); - - // Abstract tree has it's own implementation of triggering custom hover - // TreeView uses it's own implementation due to setting focus inside the (markdown) - let showTreeHoverCancellation = new CancellationTokenSource(); - KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'workbench.action.showTreeHover', - handler: async (accessor: ServicesAccessor, ...args: any[]) => { - showTreeHoverCancellation.cancel(); - showTreeHoverCancellation = new CancellationTokenSource(); - const listService = accessor.get(IListService); - const treeViewsService = accessor.get(ITreeViewsService); - const hoverService = accessor.get(IHoverService); - const lastFocusedList = listService.lastFocusedList; - if (!(lastFocusedList instanceof AsyncDataTree)) { - return; - } - const focus = lastFocusedList.getFocus(); - if (!focus || (focus.length === 0)) { - return; - } - const treeItem = focus[0]; - - if (treeItem instanceof ResolvableTreeItem) { - await treeItem.resolve(showTreeHoverCancellation.token); - } - if (!treeItem.tooltip) { - return; - } - const element = treeViewsService.getRenderedTreeElement(('handle' in treeItem) ? treeItem.handle : treeItem); - if (!element) { - return; - } - hoverService.showHover({ - content: treeItem.tooltip, - target: element, - position: { - hoverPosition: HoverPosition.BELOW, - }, - persistence: { - hideOnHover: false - } - }, true); - }, - weight: KeybindingWeight.WorkbenchContrib + 1, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyI), - when: ContextKeyExpr.and(RawCustomTreeViewContextKey, WorkbenchListFocusContextKey) - }); } private handleAndRegisterCustomViewContainers() { diff --git a/src/vs/workbench/browser/actions/listCommands.ts b/src/vs/workbench/browser/actions/listCommands.ts index 01de60e8080..db6890b9017 100644 --- a/src/vs/workbench/browser/actions/listCommands.ts +++ b/src/vs/workbench/browser/actions/listCommands.ts @@ -723,16 +723,29 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ return; } - // Check if the focused element has a hover, otherwise find the first child with a hover - const elementWithHover = focusedElement.matches('[custom-hover="true"]') ? focusedElement : focusedElement.querySelector('[custom-hover="true"]'); - if (!elementWithHover) { - return; + const elementWithHover = getCustomHoverForElement(focusedElement as HTMLElement); + if (elementWithHover) { + accessor.get(IHoverService).triggerUpdatableHover(elementWithHover as HTMLElement); } - - accessor.get(IHoverService).triggerUpdatableHover(elementWithHover as HTMLElement); }, }); +function getCustomHoverForElement(element: HTMLElement): HTMLElement | undefined { + // Check if the element itself has a hover + if (element.matches('[custom-hover="true"]')) { + return element; + } + + // Only consider children that are not action items or have a tabindex + // as these element are focusable and the user is able to trigger them already + const noneFocusableElementWithHover = element.querySelector('[custom-hover="true"]:not([tabindex]):not(.action-item)'); + if (noneFocusableElementWithHover) { + return noneFocusableElementWithHover as HTMLElement; + } + + return undefined; +} + KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'list.toggleExpand', weight: KeybindingWeight.WorkbenchContrib, diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index 45cf1b15b1c..becd185fd60 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -61,7 +61,6 @@ import { Extensions, ITreeItem, ITreeItemLabel, ITreeView, ITreeViewDataProvider import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IHoverService, WorkbenchHoverDelegate } from 'vs/platform/hover/browser/hover'; -import { ITreeViewsService } from 'vs/workbench/services/views/browser/treeViewsService'; import { CodeDataTransfers, LocalSelectionTransfer } from 'vs/platform/dnd/browser/dnd'; import { toExternalVSDataTransfer } from 'vs/editor/browser/dnd'; import { CheckboxStateHandler, TreeItemCheckbox } from 'vs/workbench/browser/parts/views/checkbox'; @@ -1168,7 +1167,6 @@ class TreeRenderer extends Disposable implements ITreeRenderer { } -export const ITreeViewsService = createDecorator('treeViewsService'); -registerSingleton(ITreeViewsService, TreeviewsService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/views/common/treeViewsService.ts b/src/vs/workbench/services/views/common/treeViewsService.ts deleted file mode 100644 index 4785e5fd362..00000000000 --- a/src/vs/workbench/services/views/common/treeViewsService.ts +++ /dev/null @@ -1,34 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -export interface ITreeViewsService { - readonly _serviceBrand: undefined; - - getRenderedTreeElement(node: string): V | undefined; - addRenderedTreeItemElement(node: string, element: V): void; - removeRenderedTreeItemElement(node: string): void; -} - -export class TreeviewsService implements ITreeViewsService { - _serviceBrand: undefined; - private _renderedElements: Map = new Map(); - - getRenderedTreeElement(node: string): V | undefined { - if (this._renderedElements.has(node)) { - return this._renderedElements.get(node); - } - return undefined; - } - - addRenderedTreeItemElement(node: string, element: V): void { - this._renderedElements.set(node, element); - } - - removeRenderedTreeItemElement(node: string): void { - if (this._renderedElements.has(node)) { - this._renderedElements.delete(node); - } - } -} From 3683ed71518365d2504afdb9085a1d88263f3e02 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 23 May 2024 11:03:49 +0200 Subject: [PATCH 343/357] remove `IInlineChatService` and types around it (#213298) --- .../contrib/chat/browser/chatFollowups.ts | 8 +- .../browser/inlineChat.contribution.ts | 4 +- .../browser/inlineChatController.ts | 12 +-- .../browser/inlineChatSessionServiceImpl.ts | 10 +- .../inlineChat/browser/inlineChatWidget.ts | 27 +---- .../contrib/inlineChat/common/inlineChat.ts | 100 +----------------- .../common/inlineChatServiceImpl.ts | 42 -------- .../test/browser/inlineChatController.test.ts | 4 +- .../test/browser/inlineChatSession.test.ts | 4 +- .../chat/browser/terminalChatController.ts | 1 - .../chat/browser/terminalChatWidget.ts | 1 - 11 files changed, 15 insertions(+), 198 deletions(-) delete mode 100644 src/vs/workbench/contrib/inlineChat/common/inlineChatServiceImpl.ts diff --git a/src/vs/workbench/contrib/chat/browser/chatFollowups.ts b/src/vs/workbench/contrib/chat/browser/chatFollowups.ts index eb5f7c99cdf..4f59229ff85 100644 --- a/src/vs/workbench/contrib/chat/browser/chatFollowups.ts +++ b/src/vs/workbench/contrib/chat/browser/chatFollowups.ts @@ -8,22 +8,19 @@ import { Button, IButtonStyles } from 'vs/base/browser/ui/button/button'; import { MarkdownString } from 'vs/base/common/htmlContent'; import { Disposable } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; -import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ChatAgentLocation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { chatAgentLeader, chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatFollowup } from 'vs/workbench/contrib/chat/common/chatService'; -import { IInlineChatFollowup } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; const $ = dom.$; -export class ChatFollowups extends Disposable { +export class ChatFollowups extends Disposable { constructor( container: HTMLElement, followups: T[], private readonly location: ChatAgentLocation, private readonly options: IButtonStyles | undefined, private readonly clickHandler: (followup: T) => void, - @IContextKeyService private readonly contextService: IContextKeyService, @IChatAgentService private readonly chatAgentService: IChatAgentService ) { super(); @@ -33,9 +30,6 @@ export class ChatFollowups extend } private renderFollowup(container: HTMLElement, followup: T): void { - if (followup.kind === 'command' && followup.when && !this.contextService.contextMatchesRules(ContextKeyExpr.deserialize(followup.when))) { - return; - } if (!this.chatAgentService.getDefaultAgent(this.location)) { // No default agent yet, which affects how followups are rendered, so can't render this yet diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts index 397a916905d..4ab19a40890 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts @@ -7,9 +7,8 @@ import { EditorContributionInstantiation, registerEditorContribution } from 'vs/ import { registerAction2 } from 'vs/platform/actions/common/actions'; import { InlineChatController } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; import * as InlineChatActions from 'vs/workbench/contrib/inlineChat/browser/inlineChatActions'; -import { IInlineChatService, INLINE_CHAT_ID } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { INLINE_CHAT_ID } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { InlineChatServiceImpl } from 'vs/workbench/contrib/inlineChat/common/inlineChatServiceImpl'; import { Registry } from 'vs/platform/registry/common/platform'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { InlineChatNotebookContribution } from 'vs/workbench/contrib/inlineChat/browser/inlineChatNotebook'; @@ -24,7 +23,6 @@ import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/access // --- browser -registerSingleton(IInlineChatService, InlineChatServiceImpl, InstantiationType.Delayed); registerSingleton(IInlineChatSessionService, InlineChatSessionServiceImpl, InstantiationType.Delayed); registerSingleton(IInlineChatSavingService, InlineChatSavingServiceImpl, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 98a8f4294a5..e5794444295 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -457,9 +457,9 @@ export class InlineChatController implements IEditorContribution { const result: CompletionList = { suggestions: [], incomplete: false }; for (const command of this._session.session.slashCommands) { - const withSlash = `/${command.command}`; + const withSlash = `/${command.name}`; result.suggestions.push({ - label: { label: withSlash, description: command.detail ?? '' }, + label: { label: withSlash, description: command.description ?? '' }, kind: CompletionItemKind.Text, insertText: withSlash, range: Range.fromPositions(new Position(1, 1), position), @@ -473,8 +473,8 @@ export class InlineChatController implements IEditorContribution { const updateSlashDecorations = (collection: IEditorDecorationsCollection, model: ITextModel) => { const newDecorations: IModelDeltaDecoration[] = []; - for (const command of (this._session?.session.slashCommands ?? []).sort((a, b) => b.command.length - a.command.length)) { - const withSlash = `/${command.command}`; + for (const command of (this._session?.session.slashCommands ?? []).sort((a, b) => b.name.length - a.name.length)) { + const withSlash = `/${command.name}`; const firstLine = model.getLineContent(1); if (firstLine.startsWith(withSlash)) { newDecorations.push({ @@ -490,13 +490,13 @@ export class InlineChatController implements IEditorContribution { }); // inject detail when otherwise empty - if (firstLine.trim() === `/${command.command}`) { + if (firstLine.trim() === `/${command.name}`) { newDecorations.push({ range: new Range(1, withSlash.length, 1, withSlash.length), options: { description: 'inline-chat-slash-command-detail', after: { - content: `${command.detail}`, + content: `${command.description}`, inlineClassName: 'inline-chat-slash-command-detail' } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts index 36741784114..e76cd173882 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts @@ -23,7 +23,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { DEFAULT_EDITOR_ASSOCIATION } from 'vs/workbench/common/editor'; import { ChatAgentLocation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; -import { CTX_INLINE_CHAT_HAS_PROVIDER, EditMode, IInlineChatBulkEditResponse, IInlineChatSession, IInlineChatSlashCommand, InlineChatResponseType } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_INLINE_CHAT_HAS_PROVIDER, EditMode, IInlineChatBulkEditResponse, IInlineChatSession, InlineChatResponseType } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { EmptyResponse, ErrorResponse, HunkData, ReplyResponse, Session, SessionExchange, SessionWholeRange, StashedSession, TelemetryData, TelemetryDataClassification } from './inlineChatSession'; @@ -150,15 +150,9 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { id: Math.random(), wholeRange: new Range(selection.selectionStartLineNumber, selection.selectionStartColumn, selection.positionLineNumber, selection.positionColumn), placeholder: agent.description, - slashCommands: agent.slashCommands.map(agentCommand => { - return { - command: agentCommand.name, - detail: agentCommand.description, - } satisfies IInlineChatSlashCommand; - }) + slashCommands: agent.slashCommands }; - const store = new DisposableStore(); this._logService.trace(`[IE] creating NEW session for ${editor.getId()}, ${agent.extensionId}`); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index e4cfccd30bc..efe64a7d919 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -34,11 +34,10 @@ import { asCssVariable, asCssVariableName, editorBackground, editorForeground, i import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { IAccessibleViewService } from 'vs/platform/accessibility/browser/accessibleView'; import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; -import { ChatFollowups } from 'vs/workbench/contrib/chat/browser/chatFollowups'; import { ChatModel, IChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { HunkInformation, Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; -import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_RESPONSE_FOCUSED, IInlineChatFollowup, IInlineChatSlashCommand, inlineChatBackground } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_RESPONSE_FOCUSED, inlineChatBackground } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget'; import { chatRequestBackground } from 'vs/workbench/contrib/chat/common/chatColors'; import { Selection } from 'vs/editor/common/core/selection'; @@ -133,7 +132,6 @@ export class InlineChatWidget { readonly scopedContextKeyService: IContextKeyService; - private readonly _followUpDisposables = this._store.add(new DisposableStore()); constructor( location: ChatAgentLocation, options: IInlineChatWidgetConstructionOptions, @@ -530,28 +528,6 @@ export class InlineChatWidget { } }; } - /** - * @deprecated use `setChatModel` instead - */ - updateFollowUps(items: IInlineChatFollowup[], onFollowup: (followup: IInlineChatFollowup) => void): void; - updateFollowUps(items: undefined): void; - updateFollowUps(items: IInlineChatFollowup[] | undefined, onFollowup?: ((followup: IInlineChatFollowup) => void)) { - this._followUpDisposables.clear(); - this._elements.followUps.classList.toggle('hidden', !items || items.length === 0); - reset(this._elements.followUps); - if (items && items.length > 0 && onFollowup) { - this._followUpDisposables.add( - this._instantiationService.createInstance(ChatFollowups, this._elements.followUps, items, ChatAgentLocation.Editor, undefined, onFollowup)); - } - this._onDidChangeHeight.fire(); - } - - /** - * @deprecated use `setChatModel` instead - */ - updateSlashCommands(commands: IInlineChatSlashCommand[]) { - - } updateInfo(message: string): void { this._elements.infoLabel.classList.toggle('hidden', !message); @@ -591,7 +567,6 @@ export class InlineChatWidget { reset() { this._chatWidget.saveState(); this.updateChatMessage(undefined); - this.updateFollowUps(undefined); reset(this._elements.statusLabel); this._elements.statusLabel.classList.toggle('hidden', true); diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index 2ee3979e13d..241986e3f3b 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -3,48 +3,27 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event } from 'vs/base/common/event'; import { IMarkdownString } from 'vs/base/common/htmlContent'; -import { IDisposable } from 'vs/base/common/lifecycle'; import { IRange } from 'vs/editor/common/core/range'; -import { ISelection } from 'vs/editor/common/core/selection'; import { TextEdit, WorkspaceEdit } from 'vs/editor/common/languages'; import { localize } from 'vs/nls'; import { MenuId } from 'vs/platform/actions/common/actions'; import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Registry } from 'vs/platform/registry/common/platform'; import { diffInserted, diffRemoved, editorHoverHighlight, editorWidgetBackground, editorWidgetBorder, focusBorder, inputBackground, inputPlaceholderForeground, registerColor, transparent, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; import { Extensions as ExtensionsMigration, IConfigurationMigrationRegistry } from 'vs/workbench/common/configuration'; -import { URI } from 'vs/base/common/uri'; -import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; - -export interface IInlineChatSlashCommand { - command: string; - detail?: string; -} +import { IChatAgentCommand } from 'vs/workbench/contrib/chat/common/chatAgents'; export interface IInlineChatSession { id: number; placeholder?: string; input?: string; message?: string; - slashCommands?: IInlineChatSlashCommand[]; + slashCommands?: IChatAgentCommand[]; wholeRange?: IRange; } -export interface IInlineChatRequest { - prompt: string; - selection: ISelection; - wholeRange: IRange; - attempt: number; - requestId: string; - live: boolean; - previewDocument: URI; - withIntentDetection: boolean; -} - export type IInlineChatResponse = IInlineChatEditResponse | IInlineChatBulkEditResponse; export const enum InlineChatResponseType { @@ -77,81 +56,6 @@ export interface IInlineChatBulkEditResponse { wholeRange?: IRange; } -export interface IInlineChatProgressItem { - markdownFragment?: string; - edits?: TextEdit[]; - editsShouldBeInstant?: boolean; - message?: string; - slashCommand?: string; -} - -export const enum InlineChatResponseFeedbackKind { - Unhelpful = 0, - Helpful = 1, - Undone = 2, - Accepted = 3, - Bug = 4 -} - -export interface IInlineChatReplyFollowup { - kind: 'reply'; - message: string; - title?: string; - tooltip?: string; -} - -export interface IInlineChatCommandFollowup { - kind: 'command'; - commandId: string; - args?: any[]; - title: string; // supports codicon strings - when?: string; -} - -export type IInlineChatFollowup = IInlineChatReplyFollowup | IInlineChatCommandFollowup; - -/** - * @deprecated - */ -export interface IInlineChatSessionProvider { - - extensionId: ExtensionIdentifier; - label: string; - -} - -/** - * @deprecated - */ -export const IInlineChatService = createDecorator('IInlineChatService'); - -/** - * @deprecated - */ -export interface InlineChatProviderChangeEvent { - readonly added?: IInlineChatSessionProvider; - readonly removed?: IInlineChatSessionProvider; -} - -/** - * @deprecated - */ -export interface IInlineChatService { - _serviceBrand: undefined; - - /** - * @deprecated - */ - onDidChangeProviders: Event; - /** - * @deprecated - */ - addProvider(provider: IInlineChatSessionProvider): IDisposable; - /** - * @deprecated - */ - getAllProvider(): Iterable; -} export const INLINE_CHAT_ID = 'interactiveEditor'; export const INTERACTIVE_EDITOR_ACCESSIBILITY_HELP_ID = 'interactiveEditorAccessiblityHelp'; diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChatServiceImpl.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChatServiceImpl.ts deleted file mode 100644 index ddff44a1b7f..00000000000 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChatServiceImpl.ts +++ /dev/null @@ -1,42 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { Emitter } from 'vs/base/common/event'; -import { LinkedList } from 'vs/base/common/linkedList'; -import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IInlineChatService, IInlineChatSessionProvider, CTX_INLINE_CHAT_HAS_PROVIDER, InlineChatProviderChangeEvent } from './inlineChat'; - -export class InlineChatServiceImpl implements IInlineChatService { - - declare _serviceBrand: undefined; - - private readonly _onDidChangeProviders = new Emitter(); - private readonly _entries = new LinkedList(); - private readonly _ctxHasProvider: IContextKey; - - readonly onDidChangeProviders = this._onDidChangeProviders.event; - - constructor(@IContextKeyService contextKeyService: IContextKeyService) { - this._ctxHasProvider = CTX_INLINE_CHAT_HAS_PROVIDER.bindTo(contextKeyService); - } - - addProvider(provider: IInlineChatSessionProvider): IDisposable { - - const rm = this._entries.push(provider); - this._ctxHasProvider.set(true); - this._onDidChangeProviders.fire({ added: provider }); - - return toDisposable(() => { - rm(); - this._ctxHasProvider.set(this._entries.size > 0); - this._onDidChangeProviders.fire({ removed: provider }); - }); - } - - getAllProvider() { - return [...this._entries].reverse(); - } -} 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 3262ee01ccf..497e80085f2 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts @@ -35,8 +35,7 @@ import { ChatAgentLocation, ChatAgentService, IChatAgentService } from 'vs/workb import { IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { InlineChatController, InlineChatRunOptions, State } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; import { Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; -import { CTX_INLINE_CHAT_USER_DID_EDIT, EditMode, IInlineChatService, InlineChatConfigKeys } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; -import { InlineChatServiceImpl } from 'vs/workbench/contrib/inlineChat/common/inlineChatServiceImpl'; +import { CTX_INLINE_CHAT_USER_DID_EDIT, EditMode, InlineChatConfigKeys } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; import { IInlineChatSavingService } from '../../browser/inlineChatSavingService'; import { IInlineChatSessionService } from '../../browser/inlineChatSessionService'; @@ -149,7 +148,6 @@ suite('InteractiveChatController', function () { [IEditorWorkerService, new SyncDescriptor(TestWorkerService)], [IContextKeyService, contextKeyService], [IChatAgentService, new SyncDescriptor(ChatAgentService)], - [IInlineChatService, new SyncDescriptor(InlineChatServiceImpl)], [IDiffProviderFactoryService, new SyncDescriptor(TestDiffProviderFactoryService)], [IInlineChatSessionService, new SyncDescriptor(InlineChatSessionServiceImpl)], [ICommandService, new SyncDescriptor(TestCommandService)], diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts index 9570097d78f..3731977e58b 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts @@ -30,11 +30,10 @@ import { IInlineChatSavingService } from 'vs/workbench/contrib/inlineChat/browse import { HunkState, Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; import { IInlineChatSessionService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionService'; import { InlineChatSessionServiceImpl } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl'; -import { EditMode, IInlineChatService } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { EditMode } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; import { CancellationToken } from 'vs/base/common/cancellation'; import { assertType } from 'vs/base/common/types'; -import { InlineChatServiceImpl } from 'vs/workbench/contrib/inlineChat/common/inlineChatServiceImpl'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; @@ -86,7 +85,6 @@ suite('InlineChatSession', function () { [IChatService, new SyncDescriptor(ChatService)], [IEditorWorkerService, new SyncDescriptor(TestWorkerService)], [IChatAgentService, new SyncDescriptor(ChatAgentService)], - [IInlineChatService, new SyncDescriptor(InlineChatServiceImpl)], [IContextKeyService, contextKeyService], [IDiffProviderFactoryService, new SyncDescriptor(TestDiffProviderFactoryService)], [IInlineChatSessionService, new SyncDescriptor(InlineChatSessionServiceImpl)], diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 077d3fec75c..5734afb1b87 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -314,7 +314,6 @@ export class TerminalChatController extends Disposable implements ITerminalContr try { const task = this._chatAgentService.invokeAgent(this._terminalAgentId!, requestProps, progressCallback, getHistoryEntriesFromModel(model, this._terminalAgentId!), cancellationToken); this._chatWidget?.value.inlineChatWidget.updateChatMessage(undefined); - this._chatWidget?.value.inlineChatWidget.updateFollowUps(undefined); this._chatWidget?.value.inlineChatWidget.updateProgress(true); this._chatWidget?.value.inlineChatWidget.updateInfo(GeneratingPhrase + '\u2026'); await task; diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 735be4d4ec0..8b9aedb6bc9 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -170,7 +170,6 @@ export class TerminalChatWidget extends Disposable { this._container.classList.add('hide'); this._reset(); this._inlineChatWidget.updateChatMessage(undefined); - this._inlineChatWidget.updateFollowUps(undefined); this._inlineChatWidget.updateProgress(false); this._inlineChatWidget.updateToolbar(false); this._inlineChatWidget.reset(); From d42d42e5b054ebbfd6030e75deca3e6cfb3e10e4 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 23 May 2024 11:07:47 +0200 Subject: [PATCH 344/357] increase listener refusal threshold significantly (#213292) * increase listener refusal threshold significantly * fix tests --- src/vs/base/common/event.ts | 2 +- src/vs/base/test/common/event.test.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/base/common/event.ts b/src/vs/base/common/event.ts index 4ab515e5253..f94fa3673b6 100644 --- a/src/vs/base/common/event.ts +++ b/src/vs/base/common/event.ts @@ -1065,7 +1065,7 @@ export class Emitter { */ get event(): Event { this._event ??= (callback: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => { - if (this._leakageMon && this._size > this._leakageMon.threshold * 3) { + if (this._leakageMon && this._size > this._leakageMon.threshold ** 2) { const message = `[${this._leakageMon.name}] REFUSES to accept new listeners because it exceeded its threshold by far (${this._size} vs ${this._leakageMon.threshold})`; console.warn(message); diff --git a/src/vs/base/test/common/event.test.ts b/src/vs/base/test/common/event.test.ts index 7c33d1f41af..161c7085fa6 100644 --- a/src/vs/base/test/common/event.test.ts +++ b/src/vs/base/test/common/event.test.ts @@ -376,14 +376,14 @@ suite('Event', function () { const a = ds.add(new Emitter({ onListenerError(e) { allError.push(e); }, - leakWarningThreshold: 1, + leakWarningThreshold: 3, })); - for (let i = 0; i < 5; i++) { + for (let i = 0; i < 11; i++) { a.event(() => { }, undefined, store); } - assert.deepStrictEqual(allError.length, 4); + assert.deepStrictEqual(allError.length, 5); const [start, tail] = tail2(allError); assert.ok(tail instanceof ListenerRefusalError); From 2d174613d417f1efab093cebf36417b45e82d672 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 23 May 2024 11:25:38 +0200 Subject: [PATCH 345/357] chore: Remove deprecated tokens property from vscode.proposed.chatProvider.d.ts (#213301) * chore: Remove deprecated tokens property from vscode.proposed.chatProvider.d.ts * fix compiler --- src/vs/workbench/api/common/extHostLanguageModels.ts | 4 ++-- src/vscode-dts/vscode.proposed.chatProvider.d.ts | 5 ----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/api/common/extHostLanguageModels.ts b/src/vs/workbench/api/common/extHostLanguageModels.ts index 225a0fba95b..97ee59fa601 100644 --- a/src/vs/workbench/api/common/extHostLanguageModels.ts +++ b/src/vs/workbench/api/common/extHostLanguageModels.ts @@ -159,8 +159,8 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { name: metadata.name ?? '', family: metadata.family ?? '', version: metadata.version, - maxInputTokens: metadata.maxInputTokens ?? metadata.tokens, - maxOutputTokens: metadata.maxOutputTokens ?? metadata.tokens, + maxInputTokens: metadata.maxInputTokens, + maxOutputTokens: metadata.maxOutputTokens, auth, targetExtensions: metadata.extensions }); diff --git a/src/vscode-dts/vscode.proposed.chatProvider.d.ts b/src/vscode-dts/vscode.proposed.chatProvider.d.ts index 1939929db22..7fc7c3e3b97 100644 --- a/src/vscode-dts/vscode.proposed.chatProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatProvider.d.ts @@ -48,11 +48,6 @@ declare module 'vscode' { readonly maxOutputTokens: number; - /** - * @deprecated - */ - tokens: number; - /** * When present, this gates the use of `requestLanguageModelAccess` behind an authorization flow where * the user must approve of another extension accessing the models contributed by this extension. From 027fce3efdd48232c3d8e0eab7abb3c3a3e38d43 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Thu, 23 May 2024 11:45:56 +0200 Subject: [PATCH 346/357] Polish the code that sets env in packageJSONContribution.ts (#213306) --- extensions/npm/src/features/packageJSONContribution.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/npm/src/features/packageJSONContribution.ts b/extensions/npm/src/features/packageJSONContribution.ts index 03913d0a8de..a2f4fabcfe3 100644 --- a/extensions/npm/src/features/packageJSONContribution.ts +++ b/extensions/npm/src/features/packageJSONContribution.ts @@ -292,7 +292,7 @@ export class PackageJSONContribution implements IJSONContribution { // COREPACK_ENABLE_AUTO_PIN disables the package.json overwrite, and // COREPACK_ENABLE_PROJECT_SPEC makes the npm view command succeed // even if packageManager specified a package manager other than npm. - const env = { COREPACK_ENABLE_AUTO_PIN: "0", COREPACK_ENABLE_PROJECT_SPEC: "0" }; + const env = { ...process.env, COREPACK_ENABLE_AUTO_PIN: '0', COREPACK_ENABLE_PROJECT_SPEC: '0' }; cp.execFile(npmCommandPath, args, { cwd, env }, (error, stdout) => { if (!error) { try { From 3eb20557bd3ad145b8ef8d8ad2bbc499f1aebfb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Thu, 23 May 2024 16:27:34 +0200 Subject: [PATCH 347/357] fix this reference in instantiation service (#213311) --- src/vs/platform/instantiation/common/instantiationService.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/platform/instantiation/common/instantiationService.ts b/src/vs/platform/instantiation/common/instantiationService.ts index 5815924b360..4b313ed32eb 100644 --- a/src/vs/platform/instantiation/common/instantiationService.ts +++ b/src/vs/platform/instantiation/common/instantiationService.ts @@ -73,9 +73,10 @@ export class InstantiationService implements IInstantiationService { createChild(services: ServiceCollection, store?: DisposableStore): IInstantiationService { this._throwIfDisposed(); + const that = this; const result = new class extends InstantiationService { override dispose(): void { - this._children.delete(result); + that._children.delete(result); super.dispose(); } }(services, this._strict, this, this._enableTracing); From 343a048566eeedc906c728bd8a9a3e0c357761e0 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Thu, 23 May 2024 17:31:20 +0200 Subject: [PATCH 348/357] Fix unsafe type assertions on object literals (#213318) --- src/vs/platform/theme/browser/defaultStyles.ts | 4 ++-- src/vs/server/node/webClientServer.ts | 12 ++++++------ .../contrib/themes/browser/themes.contribution.ts | 8 ++++---- .../services/themes/common/colorThemeData.ts | 4 ++-- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/vs/platform/theme/browser/defaultStyles.ts b/src/vs/platform/theme/browser/defaultStyles.ts index 7549840266a..871178faf38 100644 --- a/src/vs/platform/theme/browser/defaultStyles.ts +++ b/src/vs/platform/theme/browser/defaultStyles.ts @@ -21,8 +21,8 @@ export type IStyleOverride = { [P in keyof T]?: ColorIdentifier | undefined; }; -function overrideStyles(override: IStyleOverride, styles: T): any { - const result = { ...styles } as { [P in keyof T]: string | undefined }; +function overrideStyles(override: IStyleOverride, styles: T): any { + const result: { [P in keyof T]: string | undefined } = { ...styles }; for (const key in override) { const val = override[key]; result[key] = val !== undefined ? asCssVariable(val) : undefined; diff --git a/src/vs/server/node/webClientServer.ts b/src/vs/server/node/webClientServer.ts index f0c43c66aef..6dc550b6cf0 100644 --- a/src/vs/server/node/webClientServer.ts +++ b/src/vs/server/node/webClientServer.ts @@ -30,13 +30,13 @@ import { isString } from 'vs/base/common/types'; import { CharCode } from 'vs/base/common/charCode'; import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; -const textMimeType = { +const textMimeType: { [ext: string]: string | undefined } = { '.html': 'text/html', '.js': 'text/javascript', '.json': 'application/json', '.css': 'text/css', '.svg': 'image/svg+xml', -} as { [ext: string]: string | undefined }; +}; /** * Return an error to the client. @@ -306,17 +306,17 @@ export class WebClientServer { scopes: [['user:email'], ['repo']] } : undefined; - const productConfiguration = >{ + const productConfiguration = { embedderIdentifier: 'server-distro', - extensionsGallery: this._webExtensionResourceUrlTemplate ? { + extensionsGallery: this._webExtensionResourceUrlTemplate && this._productService.extensionsGallery ? { ...this._productService.extensionsGallery, - 'resourceUrlTemplate': this._webExtensionResourceUrlTemplate.with({ + resourceUrlTemplate: this._webExtensionResourceUrlTemplate.with({ scheme: 'http', authority: remoteAuthority, path: `${this._webExtensionRoute}/${this._webExtensionResourceUrlTemplate.authority}${this._webExtensionResourceUrlTemplate.path}` }).toString(true) } : undefined - }; + } satisfies Partial; if (!this._environmentService.isBuilt) { try { diff --git a/src/vs/workbench/contrib/themes/browser/themes.contribution.ts b/src/vs/workbench/contrib/themes/browser/themes.contribution.ts index 24293e2bde3..def265bcd48 100644 --- a/src/vs/workbench/contrib/themes/browser/themes.contribution.ts +++ b/src/vs/workbench/contrib/themes/browser/themes.contribution.ts @@ -815,18 +815,18 @@ registerAction2(class extends Action2 { }); const ThemesSubMenu = new MenuId('ThemesSubMenu'); -MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { +MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { title: localize('themes', "Themes"), submenu: ThemesSubMenu, group: '2_configuration', order: 7 -}); -MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { +} satisfies ISubmenuItem); +MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { title: localize({ key: 'miSelectTheme', comment: ['&& denotes a mnemonic'] }, "&&Theme"), submenu: ThemesSubMenu, group: '2_configuration', order: 7 -}); +} satisfies ISubmenuItem); MenuRegistry.appendMenuItem(ThemesSubMenu, { command: { diff --git a/src/vs/workbench/services/themes/common/colorThemeData.ts b/src/vs/workbench/services/themes/common/colorThemeData.ts index c498d6c4da4..dd7a8f0d03f 100644 --- a/src/vs/workbench/services/themes/common/colorThemeData.ts +++ b/src/vs/workbench/services/themes/common/colorThemeData.ts @@ -437,7 +437,7 @@ export class ColorThemeData implements IWorkbenchColorTheme { } public getThemeSpecificColors(colors: IThemeScopableCustomizations): IThemeScopedCustomizations | undefined { - let themeSpecificColors; + let themeSpecificColors: IThemeScopedCustomizations | undefined; for (const key in colors) { const scopedColors = colors[key]; if (this.isThemeScope(key) && scopedColors instanceof Object && !Array.isArray(scopedColors)) { @@ -446,7 +446,7 @@ export class ColorThemeData implements IWorkbenchColorTheme { const themeId = themeScope.substring(1, themeScope.length - 1); if (this.isThemeScopeMatch(themeId)) { if (!themeSpecificColors) { - themeSpecificColors = {} as IThemeScopedCustomizations; + themeSpecificColors = {}; } const scopedThemeSpecificColors = scopedColors as IThemeScopedCustomizations; for (const subkey in scopedThemeSpecificColors) { From ca45e3d78eb45857ee12036794dc636e2da7e490 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 23 May 2024 17:52:39 +0200 Subject: [PATCH 349/357] Benibenj/registerContextKeyHandler (#213135) * cleanup editor group context keys * Update src/vs/workbench/browser/parts/editor/editorPart.ts Co-authored-by: Benjamin Pasero * context key on parts * Update global context keys * remove scoped keys on group removal * cleanup * first draft contexkt key registration * Make it a provider * Use group instead of active editor * getGroupContextKeyValue * doc * Fix merge error * :lipstick: --------- Co-authored-by: Benjamin Pasero --- .../browser/parts/editor/editorParts.ts | 123 +++++++++--- .../workbench/contrib/scm/browser/activity.ts | 66 +++++-- .../editor/common/editorGroupsService.ts | 28 ++- .../test/browser/editorGroupsService.test.ts | 182 +++++++++++++++++- .../test/browser/workbenchTestServices.ts | 34 ++-- 5 files changed, 365 insertions(+), 68 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/editorParts.ts b/src/vs/workbench/browser/parts/editor/editorParts.ts index 6b7a1907005..574c97e4153 100644 --- a/src/vs/workbench/browser/parts/editor/editorParts.ts +++ b/src/vs/workbench/browser/parts/editor/editorParts.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { EditorGroupLayout, GroupDirection, GroupLocation, GroupOrientation, GroupsArrangement, GroupsOrder, IAuxiliaryEditorPart, IAuxiliaryEditorPartCreateEvent, IEditorDropTargetDelegate, IEditorGroupsService, IEditorSideGroup, IEditorWorkingSet, IFindGroupScope, IMergeGroupOptions } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { EditorGroupLayout, GroupDirection, GroupLocation, GroupOrientation, GroupsArrangement, GroupsOrder, IAuxiliaryEditorPart, IAuxiliaryEditorPartCreateEvent, IEditorGroupContextKeyProvider, IEditorDropTargetDelegate, IEditorGroupsService, IEditorSideGroup, IEditorWorkingSet, IFindGroupScope, IMergeGroupOptions } from 'vs/workbench/services/editor/common/editorGroupsService'; import { Emitter } from 'vs/base/common/event'; -import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { GroupIdentifier } from 'vs/workbench/common/editor'; import { EditorPart, IEditorPartUIState, MainEditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; import { IEditorGroupView, IEditorPartsView } from 'vs/workbench/browser/parts/editor/editor'; @@ -46,7 +46,7 @@ export class EditorParts extends MultiWindowParts implements IEditor private mostRecentActiveParts = [this.mainPart]; constructor( - @IInstantiationService private readonly instantiationService: IInstantiationService, + @IInstantiationService protected readonly instantiationService: IInstantiationService, @IStorageService private readonly storageService: IStorageService, @IThemeService themeService: IThemeService, @IAuxiliaryWindowService private readonly auxiliaryWindowService: IAuxiliaryWindowService, @@ -62,6 +62,7 @@ export class EditorParts extends MultiWindowParts implements IEditor private registerListeners(): void { this._register(this.onDidChangeMementoValue(StorageScope.WORKSPACE, this._store)(e => this.onDidChangeMementoState(e))); + this.whenReady.then(() => this.registerGroupsContextKeyListeners()); } protected createMainEditorPart(): MainEditorPart { @@ -123,15 +124,9 @@ export class EditorParts extends MultiWindowParts implements IEditor })); disposables.add(toDisposable(() => this.doUpdateMostRecentActive(part))); - disposables.add(part.onDidChangeActiveGroup(group => { - this.updateGlobalContextKeys(); - this._onDidActiveGroupChange.fire(group); - })); + disposables.add(part.onDidChangeActiveGroup(group => this._onDidActiveGroupChange.fire(group))); disposables.add(part.onDidAddGroup(group => this._onDidAddGroup.fire(group))); - disposables.add(part.onDidRemoveGroup(group => { - this.removeGroupScopedContextKeys(group); - this._onDidRemoveGroup.fire(group); - })); + disposables.add(part.onDidRemoveGroup(group => this._onDidRemoveGroup.fire(group))); disposables.add(part.onDidMoveGroup(group => this._onDidMoveGroup.fire(group))); disposables.add(part.onDidActivateGroup(group => this._onDidActivateGroup.fire(group))); disposables.add(part.onDidChangeGroupMaximized(maximized => this._onDidChangeGroupMaximized.fire(maximized))); @@ -456,7 +451,7 @@ export class EditorParts extends MultiWindowParts implements IEditor //#endregion - //#region Editor Groups Service + //#region Group Management get activeGroup(): IEditorGroupView { return this.activePart.activeGroup; @@ -635,9 +630,40 @@ export class EditorParts extends MultiWindowParts implements IEditor return this.getPart(container).createEditorDropTarget(container, delegate); } + //#endregion + + //#region Editor Group Context Key Handling + private readonly globalContextKeys = new Map>(); private readonly scopedContextKeys = new Map>>(); + private registerGroupsContextKeyListeners(): void { + this._register(this.onDidChangeActiveGroup(() => this.updateGlobalContextKeys())); + this.groups.forEach(group => this.registerGroupContextKeyProvidersListeners(group)); + this._register(this.onDidAddGroup(group => this.registerGroupContextKeyProvidersListeners(group))); + this._register(this.onDidRemoveGroup(group => { + this.scopedContextKeys.delete(group.id); + this.registeredContextKeys.delete(group.id); + this.contextKeyProviderDisposables.deleteAndDispose(group.id); + })); + } + + private updateGlobalContextKeys(): void { + const activeGroupScopedContextKeys = this.scopedContextKeys.get(this.activeGroup.id); + if (!activeGroupScopedContextKeys) { + return; + } + + for (const [key, globalContextKey] of this.globalContextKeys) { + const scopedContextKey = activeGroupScopedContextKeys.get(key); + if (scopedContextKey) { + globalContextKey.set(scopedContextKey.get()); + } else { + globalContextKey.reset(); + } + } + } + bind(contextKey: RawContextKey, group: IEditorGroupView): IContextKey { // Ensure we only bind to the same context key once globaly @@ -679,27 +705,70 @@ export class EditorParts extends MultiWindowParts implements IEditor }; } - private updateGlobalContextKeys(): void { - const activeGroupScopedContextKeys = this.scopedContextKeys.get(this.activeGroup.id); - if (!activeGroupScopedContextKeys) { - return; + private readonly contextKeyProviders = new Map>(); + private readonly registeredContextKeys = new Map>(); + + registerContextKeyProvider(provider: IEditorGroupContextKeyProvider): IDisposable { + if (this.contextKeyProviders.has(provider.contextKey.key) || this.globalContextKeys.has(provider.contextKey.key)) { + throw new Error(`A context key provider for key ${provider.contextKey.key} already exists.`); } - for (const [key, globalContextKey] of this.globalContextKeys) { - const scopedContextKey = activeGroupScopedContextKeys.get(key); - if (scopedContextKey) { - globalContextKey.set(scopedContextKey.get()); - } else { - globalContextKey.reset(); + this.contextKeyProviders.set(provider.contextKey.key, provider); + + const setContextKeyForGroups = () => { + for (const group of this.groups) { + this.updateRegisteredContextKey(group, provider); } - } + }; + + // Run initially and on change + setContextKeyForGroups(); + const onDidChange = provider.onDidChange?.(() => setContextKeyForGroups()); + + return toDisposable(() => { + onDidChange?.dispose(); + + this.globalContextKeys.delete(provider.contextKey.key); + this.scopedContextKeys.forEach(scopedContextKeys => scopedContextKeys.delete(provider.contextKey.key)); + + this.contextKeyProviders.delete(provider.contextKey.key); + this.registeredContextKeys.forEach(registeredContextKeys => registeredContextKeys.delete(provider.contextKey.key)); + }); } - private removeGroupScopedContextKeys(group: IEditorGroupView): void { - const groupScopedContextKeys = this.scopedContextKeys.get(group.id); - if (groupScopedContextKeys) { - this.scopedContextKeys.delete(group.id); + private readonly contextKeyProviderDisposables = this._register(new DisposableMap()); + private registerGroupContextKeyProvidersListeners(group: IEditorGroupView): void { + + // Update context keys from providers for the group when its active editor changes + const disposable = group.onDidActiveEditorChange(() => { + for (const contextKeyProvider of this.contextKeyProviders.values()) { + this.updateRegisteredContextKey(group, contextKeyProvider); + } + }); + + this.contextKeyProviderDisposables.set(group.id, disposable); + } + + private updateRegisteredContextKey(group: IEditorGroupView, provider: IEditorGroupContextKeyProvider): void { + + // Get the group scoped context keys for the provider + // If the providers context key has not yet been bound + // to the group, do so now. + + let groupRegisteredContextKeys = this.registeredContextKeys.get(group.id); + if (!groupRegisteredContextKeys) { + groupRegisteredContextKeys = new Map(); + this.scopedContextKeys.set(group.id, groupRegisteredContextKeys); } + + let scopedRegisteredContextKey = groupRegisteredContextKeys.get(provider.contextKey.key); + if (!scopedRegisteredContextKey) { + scopedRegisteredContextKey = this.bind(provider.contextKey, group); + groupRegisteredContextKeys.set(provider.contextKey.key, scopedRegisteredContextKey); + } + + // Set the context key value for the group context + scopedRegisteredContextKey.set(provider.getGroupContextKeyValue(group)); } //#endregion diff --git a/src/vs/workbench/contrib/scm/browser/activity.ts b/src/vs/workbench/contrib/scm/browser/activity.ts index 088eec24d7c..7ee1d516f6d 100644 --- a/src/vs/workbench/contrib/scm/browser/activity.ts +++ b/src/vs/workbench/contrib/scm/browser/activity.ts @@ -6,7 +6,7 @@ import { localize } from 'vs/nls'; import { basename } from 'vs/base/common/resources'; import { IDisposable, dispose, Disposable, DisposableStore, combinedDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; -import { Event } from 'vs/base/common/event'; +import { Emitter, Event } from 'vs/base/common/event'; import { VIEW_PANE_ID, ISCMService, ISCMRepository, ISCMViewService } from 'vs/workbench/contrib/scm/common/scm'; import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; @@ -19,6 +19,8 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' import { Schemas } from 'vs/base/common/network'; import { Iterable } from 'vs/base/common/iterator'; import { ITitleService } from 'vs/workbench/services/title/browser/titleService'; +import { IEditorGroupContextKeyProvider, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; function getCount(repository: ISCMRepository): number { if (typeof repository.provider.count === 'number') { @@ -291,19 +293,17 @@ export class SCMActiveRepositoryContextKeyController implements IWorkbenchContri export class SCMActiveResourceContextKeyController implements IWorkbenchContribution { - private activeResourceHasChangesContextKey: IContextKey; - private activeResourceRepositoryContextKey: IContextKey; private readonly disposables = new DisposableStore(); private repositoryDisposables = new Set(); + private onDidRepositoryChange = new Emitter(); constructor( - @IContextKeyService contextKeyService: IContextKeyService, - @IEditorService private readonly editorService: IEditorService, + @IEditorGroupsService editorGroupsService: IEditorGroupsService, @ISCMService private readonly scmService: ISCMService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService ) { - this.activeResourceHasChangesContextKey = contextKeyService.createKey('scmActiveResourceHasChanges', false); - this.activeResourceRepositoryContextKey = contextKeyService.createKey('scmActiveResourceRepository', undefined); + const activeResourceHasChangesContextKey = new RawContextKey('scmActiveResourceHasChanges', false, localize('scmActiveResourceHasChanges', "Whether the active resource has changes")); + const activeResourceRepositoryContextKey = new RawContextKey('scmActiveResourceRepository', undefined, localize('scmActiveResourceRepository', "The active resource's repository")); this.scmService.onDidAddRepository(this.onDidAddRepository, this, this.disposables); @@ -311,26 +311,42 @@ export class SCMActiveResourceContextKeyController implements IWorkbenchContribu this.onDidAddRepository(repository); } - editorService.onDidActiveEditorChange(this.updateContextKey, this, this.disposables); + // Create context key providers which will update the context keys based on each groups active editor + const hasChangesContextKeyProvider: IEditorGroupContextKeyProvider = { + contextKey: activeResourceHasChangesContextKey, + getGroupContextKeyValue: (group) => this.getEditorHasChanges(group.activeEditor), + onDidChange: this.onDidRepositoryChange.event + }; + + const repositoryContextKeyProvider: IEditorGroupContextKeyProvider = { + contextKey: activeResourceRepositoryContextKey, + getGroupContextKeyValue: (group) => this.getEditorRepositoryId(group.activeEditor), + onDidChange: this.onDidRepositoryChange.event + }; + + this.disposables.add(editorGroupsService.registerContextKeyProvider(hasChangesContextKeyProvider)); + this.disposables.add(editorGroupsService.registerContextKeyProvider(repositoryContextKeyProvider)); } private onDidAddRepository(repository: ISCMRepository): void { const onDidChange = Event.any(repository.provider.onDidChange, repository.provider.onDidChangeResources); - const changeDisposable = onDidChange(() => this.updateContextKey()); + const changeDisposable = onDidChange(() => { + this.onDidRepositoryChange.fire(); + }); const onDidRemove = Event.filter(this.scmService.onDidRemoveRepository, e => e === repository); const removeDisposable = onDidRemove(() => { disposable.dispose(); this.repositoryDisposables.delete(disposable); - this.updateContextKey(); + this.onDidRepositoryChange.fire(); }); const disposable = combinedDisposable(changeDisposable, removeDisposable); this.repositoryDisposables.add(disposable); } - private updateContextKey(): void { - const activeResource = EditorResourceAccessor.getOriginalUri(this.editorService.activeEditor); + private getEditorRepositoryId(activeEditor: EditorInput | null): string | undefined { + const activeResource = EditorResourceAccessor.getOriginalUri(activeEditor); if (activeResource?.scheme === Schemas.file || activeResource?.scheme === Schemas.vscodeRemote) { const activeResourceRepository = Iterable.find( @@ -338,27 +354,37 @@ export class SCMActiveResourceContextKeyController implements IWorkbenchContribu r => Boolean(r.provider.rootUri && this.uriIdentityService.extUri.isEqualOrParent(activeResource, r.provider.rootUri)) ); - this.activeResourceRepositoryContextKey.set(activeResourceRepository?.id); + return activeResourceRepository?.id; + } + + return undefined; + } + + private getEditorHasChanges(activeEditor: EditorInput | null): boolean { + const activeResource = EditorResourceAccessor.getOriginalUri(activeEditor); + + if (activeResource?.scheme === Schemas.file || activeResource?.scheme === Schemas.vscodeRemote) { + const activeResourceRepository = Iterable.find( + this.scmService.repositories, + r => Boolean(r.provider.rootUri && this.uriIdentityService.extUri.isEqualOrParent(activeResource, r.provider.rootUri)) + ); for (const resourceGroup of activeResourceRepository?.provider.groups ?? []) { if (resourceGroup.resources .some(scmResource => this.uriIdentityService.extUri.isEqual(activeResource, scmResource.sourceUri))) { - this.activeResourceHasChangesContextKey.set(true); - return; + return true; } } - - this.activeResourceHasChangesContextKey.set(false); - } else { - this.activeResourceHasChangesContextKey.set(false); - this.activeResourceRepositoryContextKey.set(undefined); } + + return false; } dispose(): void { this.disposables.dispose(); dispose(this.repositoryDisposables.values()); this.repositoryDisposables.clear(); + this.onDidRepositoryChange.dispose(); } } diff --git a/src/vs/workbench/services/editor/common/editorGroupsService.ts b/src/vs/workbench/services/editor/common/editorGroupsService.ts index 8670f77ece2..6a7acef2ae6 100644 --- a/src/vs/workbench/services/editor/common/editorGroupsService.ts +++ b/src/vs/workbench/services/editor/common/editorGroupsService.ts @@ -11,7 +11,7 @@ import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IDimension } from 'vs/editor/common/core/dimension'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyValue, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { URI } from 'vs/base/common/uri'; import { IGroupModelChangeEvent } from 'vs/workbench/common/editor/editorGroupModel'; import { IRectangle } from 'vs/platform/window/common/window'; @@ -491,6 +491,24 @@ export interface IEditorWorkingSet { readonly name: string; } +export interface IEditorGroupContextKeyProvider { + + /** + * The context key that needs to be set for each editor group context and the global context. + */ + readonly contextKey: RawContextKey; + + /** + * Retrieves the context key value for the given editor group. + */ + readonly getGroupContextKeyValue: (group: IEditorGroup) => T; + + /** + * An event that is fired when there was a change leading to the context key value to be re-evaluated. + */ + readonly onDidChange?: Event; +} + /** * The main service to interact with editor groups across all opened editor parts. */ @@ -561,6 +579,14 @@ export interface IEditorGroupsService extends IEditorGroupsContainer { * Deletes a working set. */ deleteWorkingSet(workingSet: IEditorWorkingSet): void; + + /** + * Registers a context key provider. This provider sets a context key for each scoped editor group context and the global context. + * + * @param provider - The context key provider to be registered. + * @returns - A disposable object to unregister the provider. + */ + registerContextKeyProvider(provider: IEditorGroupContextKeyProvider): IDisposable; } export const enum OpenEditorContext { diff --git a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts index f458db5c34f..80455d79ac1 100644 --- a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { workbenchInstantiationService, registerTestEditor, TestFileEditorInput, TestEditorPart, TestServiceAccessor, createEditorPart, ITestInstantiationService, workbenchTeardown } from 'vs/workbench/test/browser/workbenchTestServices'; -import { GroupDirection, GroupsOrder, MergeGroupMode, GroupOrientation, GroupLocation, isEditorGroup, IEditorGroupsService, GroupsArrangement } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { workbenchInstantiationService, registerTestEditor, TestFileEditorInput, TestEditorPart, TestServiceAccessor, ITestInstantiationService, workbenchTeardown, createEditorParts, TestEditorParts } from 'vs/workbench/test/browser/workbenchTestServices'; +import { GroupDirection, GroupsOrder, MergeGroupMode, GroupOrientation, GroupLocation, isEditorGroup, IEditorGroupsService, GroupsArrangement, IEditorGroupContextKeyProvider } from 'vs/workbench/services/editor/common/editorGroupsService'; import { CloseDirection, IEditorPartOptions, EditorsOrder, EditorInputCapabilities, GroupModelChangeKind, SideBySideEditor, IEditorFactoryRegistry, EditorExtensions } from 'vs/workbench/common/editor'; import { URI } from 'vs/base/common/uri'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; @@ -19,6 +19,9 @@ import { IGroupModelChangeEvent, IGroupEditorMoveEvent, IGroupEditorOpenEvent } import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Registry } from 'vs/platform/registry/common/platform'; +import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { Emitter } from 'vs/base/common/event'; +import { isEqual } from 'vs/base/common/resources'; suite('EditorGroupsService', () => { @@ -42,14 +45,19 @@ suite('EditorGroupsService', () => { disposables.clear(); }); - async function createPart(instantiationService = workbenchInstantiationService(undefined, disposables)): Promise<[TestEditorPart, TestInstantiationService]> { + async function createParts(instantiationService = workbenchInstantiationService(undefined, disposables)): Promise<[TestEditorParts, TestInstantiationService]> { instantiationService.invokeFunction(accessor => Registry.as(EditorExtensions.EditorFactory).start(accessor)); - const part = await createEditorPart(instantiationService, disposables); - instantiationService.stub(IEditorGroupsService, part); + const parts = await createEditorParts(instantiationService, disposables); + instantiationService.stub(IEditorGroupsService, parts); testLocalInstantiationService = instantiationService; - return [part, instantiationService]; + return [parts, instantiationService]; + } + + async function createPart(instantiationService?: TestInstantiationService): Promise<[TestEditorPart, TestInstantiationService]> { + const [parts, testInstantiationService] = await createParts(instantiationService); + return [parts.testMainPart, testInstantiationService]; } function createTestFileEditorInput(resource: URI, typeId: string): TestFileEditorInput { @@ -2027,5 +2035,167 @@ suite('EditorGroupsService', () => { assert.strictEqual(part.activeGroup.isEmpty, true); }); + test('context key provider', async function () { + const disposables = new DisposableStore(); + + // Instantiate workbench and setup initial state + const instantiationService = workbenchInstantiationService({ contextKeyService: instantiationService => instantiationService.createInstance(MockScopableContextKeyService) }, disposables); + const rootContextKeyService = instantiationService.get(IContextKeyService); + + const [parts] = await createParts(instantiationService); + + const input1 = createTestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); + const input2 = createTestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); + const input3 = createTestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); + + const group1 = parts.activeGroup; + const group2 = parts.addGroup(group1, GroupDirection.RIGHT); + + await group2.openEditor(input2, { pinned: true }); + await group1.openEditor(input1, { pinned: true }); + + // Create context key provider + const rawContextKey = new RawContextKey('testContextKey', parts.activeGroup.id); + const contextKeyProvider: IEditorGroupContextKeyProvider = { + contextKey: rawContextKey, + getGroupContextKeyValue: (group) => group.id + }; + disposables.add(parts.registerContextKeyProvider(contextKeyProvider)); + + // Initial state: group1 is active + assert.strictEqual(parts.activeGroup.id, group1.id); + + let globalContextKeyValue = rootContextKeyService.getContextKeyValue(rawContextKey.key); + let group1ContextKeyValue = group1.scopedContextKeyService.getContextKeyValue(rawContextKey.key); + let group2ContextKeyValue = group2.scopedContextKeyService.getContextKeyValue(rawContextKey.key); + assert.strictEqual(globalContextKeyValue, group1.id); + assert.strictEqual(group1ContextKeyValue, group1.id); + assert.strictEqual(group2ContextKeyValue, group2.id); + + // Make group2 active and ensure both gloabal and local context key values are updated + parts.activateGroup(group2); + + globalContextKeyValue = rootContextKeyService.getContextKeyValue(rawContextKey.key); + group1ContextKeyValue = group1.scopedContextKeyService.getContextKeyValue(rawContextKey.key); + group2ContextKeyValue = group2.scopedContextKeyService.getContextKeyValue(rawContextKey.key); + assert.strictEqual(globalContextKeyValue, group2.id); + assert.strictEqual(group1ContextKeyValue, group1.id); + assert.strictEqual(group2ContextKeyValue, group2.id); + + // Add a new group and ensure both gloabal and local context key values are updated + // Group 3 will be active + const group3 = parts.addGroup(group2, GroupDirection.RIGHT); + await group3.openEditor(input3, { pinned: true }); + + globalContextKeyValue = rootContextKeyService.getContextKeyValue(rawContextKey.key); + group1ContextKeyValue = group1.scopedContextKeyService.getContextKeyValue(rawContextKey.key); + group2ContextKeyValue = group2.scopedContextKeyService.getContextKeyValue(rawContextKey.key); + const group3ContextKeyValue = group3.scopedContextKeyService.getContextKeyValue(rawContextKey.key); + assert.strictEqual(globalContextKeyValue, group3.id); + assert.strictEqual(group1ContextKeyValue, group1.id); + assert.strictEqual(group2ContextKeyValue, group2.id); + assert.strictEqual(group3ContextKeyValue, group3.id); + + disposables.dispose(); + }); + + test('context key provider: onDidChange', async function () { + const disposables = new DisposableStore(); + + // Instantiate workbench and setup initial state + const instantiationService = workbenchInstantiationService({ contextKeyService: instantiationService => instantiationService.createInstance(MockScopableContextKeyService) }, disposables); + const rootContextKeyService = instantiationService.get(IContextKeyService); + + const parts = await createEditorParts(instantiationService, disposables); + + const input1 = createTestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); + const input2 = createTestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); + + const group1 = parts.activeGroup; + const group2 = parts.addGroup(group1, GroupDirection.RIGHT); + + await group2.openEditor(input2, { pinned: true }); + await group1.openEditor(input1, { pinned: true }); + + // Create context key provider + let offset = 0; + const _onDidChange = new Emitter(); + + const rawContextKey = new RawContextKey('testContextKey', parts.activeGroup.id); + const contextKeyProvider: IEditorGroupContextKeyProvider = { + contextKey: rawContextKey, + getGroupContextKeyValue: (group) => group.id + offset, + onDidChange: _onDidChange.event + }; + disposables.add(parts.registerContextKeyProvider(contextKeyProvider)); + + // Initial state: group1 is active + assert.strictEqual(parts.activeGroup.id, group1.id); + + let globalContextKeyValue = rootContextKeyService.getContextKeyValue(rawContextKey.key); + let group1ContextKeyValue = group1.scopedContextKeyService.getContextKeyValue(rawContextKey.key); + let group2ContextKeyValue = group2.scopedContextKeyService.getContextKeyValue(rawContextKey.key); + assert.strictEqual(globalContextKeyValue, group1.id + offset); + assert.strictEqual(group1ContextKeyValue, group1.id + offset); + assert.strictEqual(group2ContextKeyValue, group2.id + offset); + + // Make a change to the context key provider and fire onDidChange such that all context key values are updated + offset = 10; + _onDidChange.fire(); + + globalContextKeyValue = rootContextKeyService.getContextKeyValue(rawContextKey.key); + group1ContextKeyValue = group1.scopedContextKeyService.getContextKeyValue(rawContextKey.key); + group2ContextKeyValue = group2.scopedContextKeyService.getContextKeyValue(rawContextKey.key); + assert.strictEqual(globalContextKeyValue, group1.id + offset); + assert.strictEqual(group1ContextKeyValue, group1.id + offset); + assert.strictEqual(group2ContextKeyValue, group2.id + offset); + + disposables.dispose(); + }); + + test('context key provider: active editor change', async function () { + const disposables = new DisposableStore(); + + // Instantiate workbench and setup initial state + const instantiationService = workbenchInstantiationService({ contextKeyService: instantiationService => instantiationService.createInstance(MockScopableContextKeyService) }, disposables); + const rootContextKeyService = instantiationService.get(IContextKeyService); + + const parts = await createEditorParts(instantiationService, disposables); + + const input1 = createTestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); + const input2 = createTestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); + + const group1 = parts.activeGroup; + + await group1.openEditor(input2, { pinned: true }); + await group1.openEditor(input1, { pinned: true }); + + // Create context key provider + const rawContextKey = new RawContextKey('testContextKey', input1.resource.toString()); + const contextKeyProvider: IEditorGroupContextKeyProvider = { + contextKey: rawContextKey, + getGroupContextKeyValue: (group) => group.activeEditor?.resource?.toString() ?? '', + }; + disposables.add(parts.registerContextKeyProvider(contextKeyProvider)); + + // Initial state: input1 is active + assert.strictEqual(isEqual(group1.activeEditor?.resource, input1.resource), true); + + let globalContextKeyValue = rootContextKeyService.getContextKeyValue(rawContextKey.key); + let group1ContextKeyValue = group1.scopedContextKeyService.getContextKeyValue(rawContextKey.key); + assert.strictEqual(globalContextKeyValue, input1.resource.toString()); + assert.strictEqual(group1ContextKeyValue, input1.resource.toString()); + + // Make input2 active and ensure both gloabal and local context key values are updated + await group1.openEditor(input2); + + globalContextKeyValue = rootContextKeyService.getContextKeyValue(rawContextKey.key); + group1ContextKeyValue = group1.scopedContextKeyService.getContextKeyValue(rawContextKey.key); + assert.strictEqual(globalContextKeyValue, input2.resource.toString()); + assert.strictEqual(group1ContextKeyValue, input2.resource.toString()); + + disposables.dispose(); + }); + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index e8c445facde..cf3df1f56ce 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -41,7 +41,7 @@ import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService import { ITextResourceConfigurationService, ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfiguration'; import { IPosition, Position as EditorPosition } from 'vs/editor/common/core/position'; import { IMenuService, MenuId, IMenu, IMenuChangeEvent } from 'vs/platform/actions/common/actions'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyValue, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { MockContextKeyService, MockKeybindingService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { ITextBufferFactory, DefaultEndOfLine, EndOfLinePreference, ITextSnapshot } from 'vs/editor/common/model'; import { Range } from 'vs/editor/common/core/range'; @@ -52,7 +52,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IDecorationsService, IResourceDecorationChangeEvent, IDecoration, IDecorationData, IDecorationsProvider } from 'vs/workbench/services/decorations/common/decorations'; import { IDisposable, toDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { IEditorGroupsService, IEditorGroup, GroupsOrder, GroupsArrangement, GroupDirection, IMergeGroupOptions, IEditorReplacement, IFindGroupScope, EditorGroupLayout, ICloseEditorOptions, GroupOrientation, ICloseAllEditorsOptions, ICloseEditorsFilter, IEditorDropTargetDelegate, IEditorPart, IAuxiliaryEditorPart, IEditorGroupsContainer, IAuxiliaryEditorPartCreateEvent, IEditorWorkingSet } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroupsService, IEditorGroup, GroupsOrder, GroupsArrangement, GroupDirection, IMergeGroupOptions, IEditorReplacement, IFindGroupScope, EditorGroupLayout, ICloseEditorOptions, GroupOrientation, ICloseAllEditorsOptions, ICloseEditorsFilter, IEditorDropTargetDelegate, IEditorPart, IAuxiliaryEditorPart, IEditorGroupsContainer, IAuxiliaryEditorPartCreateEvent, IEditorWorkingSet, IEditorGroupContextKeyProvider } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService, ISaveEditorsOptions, IRevertAllEditorsOptions, PreferredGroup, IEditorsChangeEvent, ISaveEditorsResult } from 'vs/workbench/services/editor/common/editorService'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { IEditorPaneRegistry, EditorPaneDescriptor } from 'vs/workbench/browser/editor'; @@ -867,6 +867,7 @@ export class TestEditorGroupsService implements IEditorGroupsService { centerLayout(active: boolean): void { } isLayoutCentered(): boolean { return false; } createEditorDropTarget(container: HTMLElement, delegate: IEditorDropTargetDelegate): IDisposable { return Disposable.None; } + registerContextKeyProvider(_provider: IEditorGroupContextKeyProvider): IDisposable { throw new Error('not implemented'); } partOptions!: IEditorPartOptions; enforcePartOptions(options: IEditorPartOptions): IDisposable { return Disposable.None; } @@ -1842,28 +1843,33 @@ export class TestEditorPart extends MainEditorPart implements IEditorGroupsServi getWorkingSets(): IEditorWorkingSet[] { throw new Error('Method not implemented.'); } applyWorkingSet(workingSet: IEditorWorkingSet | 'empty'): Promise { throw new Error('Method not implemented.'); } deleteWorkingSet(workingSet: IEditorWorkingSet): Promise { throw new Error('Method not implemented.'); } + + registerContextKeyProvider(provider: IEditorGroupContextKeyProvider): IDisposable { throw new Error('Method not implemented.'); } } -export async function createEditorPart(instantiationService: IInstantiationService, disposables: DisposableStore): Promise { +export class TestEditorParts extends EditorParts { + testMainPart!: TestEditorPart; - class TestEditorParts extends EditorParts { + protected override createMainEditorPart(): MainEditorPart { + this.testMainPart = this.instantiationService.createInstance(TestEditorPart, this); - testMainPart!: TestEditorPart; - - protected override createMainEditorPart(): MainEditorPart { - this.testMainPart = instantiationService.createInstance(TestEditorPart, this); - - return this.testMainPart; - } + return this.testMainPart; } +} - const part = disposables.add(instantiationService.createInstance(TestEditorParts)).testMainPart; +export async function createEditorParts(instantiationService: IInstantiationService, disposables: DisposableStore): Promise { + const parts = instantiationService.createInstance(TestEditorParts); + const part = disposables.add(parts).testMainPart; part.create(document.createElement('div')); part.layout(1080, 800, 0, 0); - await part.whenReady; + await parts.whenReady; - return part; + return parts; +} + +export async function createEditorPart(instantiationService: IInstantiationService, disposables: DisposableStore): Promise { + return (await createEditorParts(instantiationService, disposables)).testMainPart; } export class TestListService implements IListService { From 101ea2a13ea896a99a6375dacc71addcaa9a080a Mon Sep 17 00:00:00 2001 From: Aaron Munger Date: Thu, 23 May 2024 09:12:47 -0700 Subject: [PATCH 350/357] allow returning undefined if file was not saved (#213123) * allow returning undefined if file was not saved * bring back cancellation check * helper utility to convey optional result if cancelled * edit * inline function * test cancelled custom save * inline more --------- Co-authored-by: Benjamin Pasero --- .../workbench/api/common/extHostNotebook.ts | 9 +++- .../common/storedFileWorkingCopy.ts | 20 ++++----- .../browser/storedFileWorkingCopy.test.ts | 45 +++++++++++++++++++ 3 files changed, 62 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index 0657c297678..3a7e105c843 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -335,7 +335,6 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { throw new files.FileOperationError(localize('err.readonly', "Unable to modify read-only file '{0}'", this._resourceForError(uri)), files.FileOperationResult.FILE_PERMISSION_DENIED); } - const data: vscode.NotebookData = { metadata: filter(document.apiNotebook.metadata, key => !(serializer.options?.transientDocumentMetadata ?? {})[key]), cells: [], @@ -360,7 +359,15 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { // validate write await this._validateWriteFile(uri, options); + if (token.isCancellationRequested) { + throw new Error('canceled'); + } const bytes = await serializer.serializer.serializeNotebook(data, token); + if (token.isCancellationRequested) { + throw new Error('canceled'); + } + + // Don't accept any cancellation beyond this point, we need to report the result of the file write this.trace(`serialized versionId: ${versionId} ${uri.toString()}`); await this._extHostFileSystem.value.writeFile(uri, bytes); this.trace(`Finished write versionId: ${versionId} ${uri.toString()}`); diff --git a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts index f6a173613ac..42b1eb49af3 100644 --- a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts @@ -35,16 +35,6 @@ import { IMarkdownString } from 'vs/base/common/htmlContent'; */ export interface IStoredFileWorkingCopyModelFactory extends IFileWorkingCopyModelFactory { } -export async function createOptionalResult(callback: (token: CancellationToken) => Promise, token: CancellationToken): Promise { - const result = await callback(token); - if (result === undefined && token.isCancellationRequested) { - return undefined; - } - else { - return assertIsDefined(result); - } -} - /** * The underlying model of a stored file working copy provides some * methods for the stored file working copy to function. The model is @@ -1029,7 +1019,15 @@ export class StoredFileWorkingCopy extend // Delegate to working copy model save method if any if (typeof resolvedFileWorkingCopy.model.save === 'function') { - stat = await resolvedFileWorkingCopy.model.save(writeFileOptions, saveCancellation.token); + try { + stat = await resolvedFileWorkingCopy.model.save(writeFileOptions, saveCancellation.token); + } catch (error) { + if (saveCancellation.token.isCancellationRequested) { + return undefined; // save was cancelled + } + + throw error; + } } // Otherwise ask for a snapshot and save via file services diff --git a/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts b/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts index 45f93ad9b70..d4d63ff6958 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts @@ -88,12 +88,21 @@ export class TestStoredFileWorkingCopyModelWithCustomSave extends TestStoredFile saveCounter = 0; throwOnSave = false; + saveOperation: Promise | undefined = undefined; async save(options: IWriteFileOptions, token: CancellationToken): Promise { if (this.throwOnSave) { throw new Error('Fail'); } + if (this.saveOperation) { + await this.saveOperation; + } + + if (token.isCancellationRequested) { + throw new Error('Canceled'); + } + this.saveCounter++; return { @@ -190,6 +199,42 @@ suite('StoredFileWorkingCopy (with custom save)', function () { assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.ERROR), true); }); + test('save cancelled (custom implemented)', async () => { + let savedCounter = 0; + let lastSaveEvent: IStoredFileWorkingCopySaveEvent | undefined = undefined; + disposables.add(workingCopy.onDidSave(e => { + savedCounter++; + lastSaveEvent = e; + })); + + let saveErrorCounter = 0; + disposables.add(workingCopy.onDidSaveError(() => { + saveErrorCounter++; + })); + + await workingCopy.resolve(); + let resolve: () => void; + (workingCopy.model as TestStoredFileWorkingCopyModelWithCustomSave).saveOperation = new Promise(r => resolve = r); + + workingCopy.model?.updateContents('first'); + const firstSave = workingCopy.save(); + // cancel the first save by requesting a second while it is still mid operation + workingCopy.model?.updateContents('second'); + const secondSave = workingCopy.save(); + resolve!(); + await firstSave; + await secondSave; + + assert.strictEqual(savedCounter, 1); + assert.strictEqual(saveErrorCounter, 0); + assert.strictEqual(workingCopy.isDirty(), false); + assert.strictEqual(lastSaveEvent!.reason, SaveReason.EXPLICIT); + assert.ok(lastSaveEvent!.stat); + assert.ok(isStoredFileWorkingCopySaveEvent(lastSaveEvent!)); + assert.strictEqual(workingCopy.model?.pushedStackElement, true); + assert.strictEqual((workingCopy.model as TestStoredFileWorkingCopyModelWithCustomSave).saveCounter, 1); + }); + ensureNoDisposablesAreLeakedInTestSuite(); }); From becae3cc46849c41d221a2e0ab0a30464cc90f0e Mon Sep 17 00:00:00 2001 From: David Dossett Date: Thu, 23 May 2024 11:27:39 -0700 Subject: [PATCH 351/357] Fix codicons padding in chat attachments --- src/vs/workbench/contrib/chat/browser/media/chat.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 63f14ff2492..5310cc02f75 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -507,6 +507,10 @@ color: var(--vscode-descriptionForeground); } +.interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-icon-label .codicon { + padding-left: 4px; +} + .interactive-session .chat-attached-context { padding: 0 0 8px 0; display: flex; From 6629c4e0a9232615b62e56bb3a3c0027889ac7af Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 23 May 2024 12:28:04 -0700 Subject: [PATCH 352/357] git: allow querying whether files are gitignore (#212982) * git: allow querying whether files are gitignore This exposes `checkIgnore`, which I want to use in copilot to determine which files I should go into when checking references. * rename method --- extensions/git/src/api/api1.ts | 4 ++++ extensions/git/src/api/git.d.ts | 2 ++ extensions/git/src/repository.ts | 24 ++++++++++++------------ 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/extensions/git/src/api/api1.ts b/extensions/git/src/api/api1.ts index 5d3f3c45386..f049939c137 100644 --- a/extensions/git/src/api/api1.ts +++ b/extensions/git/src/api/api1.ts @@ -192,6 +192,10 @@ export class ApiRepository implements Repository { return this.repository.getRefs(query, cancellationToken); } + checkIgnore(paths: string[]): Promise> { + return this.repository.checkIgnore(paths); + } + getMergeBase(ref1: string, ref2: string): Promise { return this.repository.getMergeBase(ref1, ref2); } diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index d6d2166e00b..685b5413947 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -235,6 +235,8 @@ export interface Repository { getBranchBase(name: string): Promise; setBranchUpstream(name: string, upstream: string): Promise; + checkIgnore(paths: string[]): Promise>; + getRefs(query: RefQuery, cancellationToken?: CancellationToken): Promise; getMergeBase(ref1: string, ref2: string): Promise; diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 4cfd44bb4f8..ed959765a59 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -3,27 +3,27 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import TelemetryReporter from '@vscode/extension-telemetry'; import * as fs from 'fs'; import * as path from 'path'; import * as picomatch from 'picomatch'; -import { CancellationToken, Command, Disposable, Event, EventEmitter, Memento, ProgressLocation, ProgressOptions, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, ThemeColor, Uri, window, workspace, WorkspaceEdit, FileDecoration, commands, TabInputTextDiff, TabInputNotebookDiff, TabInputTextMultiDiff, RelativePattern, CancellationTokenSource, LogOutputChannel, LogLevel, CancellationError, l10n } from 'vscode'; -import TelemetryReporter from '@vscode/extension-telemetry'; -import { Branch, Change, ForcePushMode, GitErrorCodes, LogOptions, Ref, Remote, Status, CommitOptions, BranchQuery, FetchOptions, RefQuery, RefType } from './api/git'; +import { CancellationError, CancellationToken, CancellationTokenSource, Command, commands, Disposable, Event, EventEmitter, FileDecoration, l10n, LogLevel, LogOutputChannel, Memento, ProgressLocation, ProgressOptions, RelativePattern, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, TabInputNotebookDiff, TabInputTextDiff, TabInputTextMultiDiff, ThemeColor, Uri, window, workspace, WorkspaceEdit } from 'vscode'; +import { ActionButton } from './actionButton'; +import { ApiRepository } from './api/api1'; +import { Branch, BranchQuery, Change, CommitOptions, FetchOptions, ForcePushMode, GitErrorCodes, LogOptions, Ref, RefQuery, RefType, Remote, Status } from './api/git'; import { AutoFetcher } from './autofetch'; +import { GitBranchProtectionProvider, IBranchProtectionProviderRegistry } from './branchProtection'; import { debounce, memoize, throttle } from './decorators'; -import { Commit, GitError, Repository as BaseRepository, Stash, Submodule, LogFileOptions, PullOptions, LsTreeElement } from './git'; +import { Repository as BaseRepository, Commit, GitError, LogFileOptions, LsTreeElement, PullOptions, Stash, Submodule } from './git'; +import { GitHistoryProvider } from './historyProvider'; +import { Operation, OperationKind, OperationManager, OperationResult } from './operation'; +import { CommitCommandsCenter, IPostCommitCommandsProviderRegistry } from './postCommitCommands'; +import { IPushErrorHandlerRegistry } from './pushError'; +import { IRemoteSourcePublisherRegistry } from './remotePublisher'; import { StatusBarCommands } from './statusbar'; import { toGitUri } from './uri'; import { anyEvent, combinedDisposable, debounceEvent, dispose, EmptyDisposable, eventToPromise, filterEvent, find, IDisposable, isDescendant, onceEvent, pathEquals, relativePath } from './util'; import { IFileWatcher, watch } from './watch'; -import { IPushErrorHandlerRegistry } from './pushError'; -import { ApiRepository } from './api/api1'; -import { IRemoteSourcePublisherRegistry } from './remotePublisher'; -import { ActionButton } from './actionButton'; -import { IPostCommitCommandsProviderRegistry, CommitCommandsCenter } from './postCommitCommands'; -import { Operation, OperationKind, OperationManager, OperationResult } from './operation'; -import { GitBranchProtectionProvider, IBranchProtectionProviderRegistry } from './branchProtection'; -import { GitHistoryProvider } from './historyProvider'; const timeout = (millis: number) => new Promise(c => setTimeout(c, millis)); From 6cf8bdb0dacf0b358b2d3288a1d083a142b2f7d8 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Thu, 23 May 2024 22:01:09 +0200 Subject: [PATCH 353/357] Making API `editorHoverVerbosityLevel` use `verbosityLevel` instead of `action` (#213317) * allowing to cancel a previous request and jump directly to a request for a delta at a higher level * adding code in order to dispose the token sources when the full object is disposed --- src/vs/editor/common/languages.ts | 4 +- .../hover/browser/markdownHoverParticipant.ts | 50 ++++++++++++------- src/vs/monaco.d.ts | 4 +- .../api/browser/mainThreadLanguageFeatures.ts | 2 +- .../api/common/extHostLanguageFeatures.ts | 2 +- ...de.proposed.editorHoverVerbosityLevel.d.ts | 4 +- 6 files changed, 41 insertions(+), 25 deletions(-) diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index 7c9e3807260..68fbf632fb4 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -202,9 +202,9 @@ export interface HoverContext { export interface HoverVerbosityRequest { /** - * Whether to increase or decrease the hover's verbosity + * The delta by which to increase/decrease the hover verbosity level */ - action: HoverVerbosityAction; + verbosityDelta: number; /** * The previous hover for the same position */ diff --git a/src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts b/src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts index c6f7896a2c7..3c11bee0849 100644 --- a/src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts +++ b/src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts @@ -5,7 +5,7 @@ import * as dom from 'vs/base/browser/dom'; import { asArray, compareBy, numberComparator } from 'vs/base/common/arrays'; -import { CancellationToken } from 'vs/base/common/cancellation'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { IMarkdownString, isEmptyMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; @@ -212,6 +212,7 @@ class MarkdownRenderedHoverParts extends Disposable { private _renderedHoverParts: RenderedHoverPart[]; private _hoverFocusInfo: FocusedHoverInfo = { hoverPartIndex: -1, focusRemains: false }; + private _ongoingHoverOperations: Map = new Map(); constructor( hoverParts: MarkdownHover[], // we own! @@ -231,6 +232,9 @@ class MarkdownRenderedHoverParts extends Disposable { renderedHoverPart.disposables.dispose(); }); })); + this._register(toDisposable(() => { + this._ongoingHoverOperations.forEach(operation => { operation.tokenSource.dispose(true); }); + })); } private _renderHoverParts( @@ -351,33 +355,45 @@ class MarkdownRenderedHoverParts extends Disposable { if (!hoverRenderedPart || !hoverRenderedPart.hoverSource?.supportsVerbosityAction(action)) { return; } - const hoverPosition = hoverRenderedPart.hoverSource.hoverPosition; - const hoverProvider = hoverRenderedPart.hoverSource.hoverProvider; - const hover = hoverRenderedPart.hoverSource.hover; - const hoverContext: HoverContext = { verbosityRequest: { action, previousHover: hover } }; - - let newHover: Hover | null | undefined; - try { - newHover = await Promise.resolve(hoverProvider.provideHover(model, hoverPosition, CancellationToken.None, hoverContext)); - } catch (e) { - onUnexpectedExternalError(e); - } + const hoverSource = hoverRenderedPart.hoverSource; + const newHover = await this._fetchHover(hoverSource, model, action); if (!newHover) { return; } - - const hoverSource = new HoverSource(newHover, hoverProvider, hoverPosition); - const renderedHoverPart = this._renderHoverPart( + const newHoverSource = new HoverSource(newHover, hoverSource.hoverProvider, hoverSource.hoverPosition); + const newHoverRenderedPart = this._renderHoverPart( hoverFocusedPartIndex, newHover.contents, - hoverSource, + newHoverSource, this._onFinishedRendering ); - this._replaceRenderedHoverPartAtIndex(hoverFocusedPartIndex, renderedHoverPart); + this._replaceRenderedHoverPartAtIndex(hoverFocusedPartIndex, newHoverRenderedPart); this._focusOnHoverPartWithIndex(hoverFocusedPartIndex); this._onFinishedRendering(); } + private async _fetchHover(hoverSource: HoverSource, model: ITextModel, action: HoverVerbosityAction): Promise { + let verbosityDelta = action === HoverVerbosityAction.Increase ? 1 : -1; + const provider = hoverSource.hoverProvider; + const ongoingHoverOperation = this._ongoingHoverOperations.get(provider); + if (ongoingHoverOperation) { + ongoingHoverOperation.tokenSource.cancel(); + verbosityDelta += ongoingHoverOperation.verbosityDelta; + } + const tokenSource = new CancellationTokenSource(); + this._ongoingHoverOperations.set(provider, { verbosityDelta, tokenSource }); + const context: HoverContext = { verbosityRequest: { verbosityDelta, previousHover: hoverSource.hover } }; + let hover: Hover | null | undefined; + try { + hover = await Promise.resolve(provider.provideHover(model, hoverSource.hoverPosition, tokenSource.token, context)); + } catch (e) { + onUnexpectedExternalError(e); + } + tokenSource.dispose(); + this._ongoingHoverOperations.delete(provider); + return hover; + } + private _replaceRenderedHoverPartAtIndex(index: number, renderedHoverPart: RenderedHoverPart): void { if (index >= this._renderHoverParts.length || index < 0) { return; diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index b9c7cbd73ab..7deb56947e1 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -6885,9 +6885,9 @@ declare namespace monaco.languages { export interface HoverVerbosityRequest { /** - * Whether to increase or decrease the hover's verbosity + * The delta by which to increase/decrease the hover verbosity level */ - action: HoverVerbosityAction; + verbosityDelta: number; /** * The previous hover for the same position */ diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 3ac703098b1..a75f2154c9d 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -260,7 +260,7 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread provideHover: async (model: ITextModel, position: EditorPosition, token: CancellationToken, context?: languages.HoverContext): Promise => { const serializedContext: languages.HoverContext<{ id: number }> = { verbosityRequest: context?.verbosityRequest ? { - action: context.verbosityRequest.action, + verbosityDelta: context.verbosityRequest.verbosityDelta, previousHover: { id: context.verbosityRequest.previousHover.id } } : undefined, }; diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 3443d97a5dd..215ff5fda37 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -277,7 +277,7 @@ class HoverAdapter { if (!previousHover) { throw new Error(`Hover with id ${previousHoverId} not found`); } - const hoverContext: vscode.HoverContext = { action: context.verbosityRequest.action, previousHover }; + const hoverContext: vscode.HoverContext = { verbosityDelta: context.verbosityRequest.verbosityDelta, previousHover }; value = await this._provider.provideHover(doc, pos, token, hoverContext); } else { value = await this._provider.provideHover(doc, pos, token); diff --git a/src/vscode-dts/vscode.proposed.editorHoverVerbosityLevel.d.ts b/src/vscode-dts/vscode.proposed.editorHoverVerbosityLevel.d.ts index fc2d55aee53..0661036c505 100644 --- a/src/vscode-dts/vscode.proposed.editorHoverVerbosityLevel.d.ts +++ b/src/vscode-dts/vscode.proposed.editorHoverVerbosityLevel.d.ts @@ -33,9 +33,9 @@ declare module 'vscode' { export interface HoverContext { /** - * Whether to increase or decrease the hover's verbosity + * The delta by which to increase/decrease the hover verbosity level */ - readonly action?: HoverVerbosityAction; + readonly verbosityDelta?: number; /** * The previous hover sent for the same position From 81e568cf865d0ab65f026fbf286884039bc7c77d Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 23 May 2024 22:09:24 +0200 Subject: [PATCH 354/357] Fix issue with Cmd+W closing the window with opened tabs (#213335) fixes #213324 --- src/vs/workbench/browser/contextkeys.ts | 64 ++++++++++++++++++- .../browser/parts/editor/editorGroupView.ts | 6 +- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/browser/contextkeys.ts b/src/vs/workbench/browser/contextkeys.ts index fb8919b8e9a..c22caa967b3 100644 --- a/src/vs/workbench/browser/contextkeys.ts +++ b/src/vs/workbench/browser/contextkeys.ts @@ -7,7 +7,7 @@ import { Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { IContextKeyService, IContextKey, setConstant as setConstantContextKey } from 'vs/platform/contextkey/common/contextkey'; import { InputFocusedContext, IsMacContext, IsLinuxContext, IsWindowsContext, IsWebContext, IsMacNativeContext, IsDevelopmentContext, IsIOSContext, ProductQualityContext, IsMobileContext } from 'vs/platform/contextkey/common/contextkeys'; -import { SplitEditorsVertically, InEditorZenModeContext, AuxiliaryBarVisibleContext, SideBarVisibleContext, PanelAlignmentContext, PanelMaximizedContext, PanelVisibleContext, EmbedderIdentifierContext, EditorTabsVisibleContext, IsMainEditorCenteredLayoutContext, MainEditorAreaVisibleContext, DirtyWorkingCopiesContext, EmptyWorkspaceSupportContext, EnterMultiRootWorkspaceSupportContext, HasWebFileSystemAccess, IsMainWindowFullscreenContext, OpenFolderWorkspaceSupportContext, RemoteNameContext, VirtualWorkspaceContext, WorkbenchStateContext, WorkspaceFolderCountContext, PanelPositionContext, TemporaryWorkspaceContext, TitleBarVisibleContext, TitleBarStyleContext, IsAuxiliaryWindowFocusedContext } from 'vs/workbench/common/contextkeys'; +import { SplitEditorsVertically, InEditorZenModeContext, AuxiliaryBarVisibleContext, SideBarVisibleContext, PanelAlignmentContext, PanelMaximizedContext, PanelVisibleContext, EmbedderIdentifierContext, EditorTabsVisibleContext, IsMainEditorCenteredLayoutContext, MainEditorAreaVisibleContext, DirtyWorkingCopiesContext, EmptyWorkspaceSupportContext, EnterMultiRootWorkspaceSupportContext, HasWebFileSystemAccess, IsMainWindowFullscreenContext, OpenFolderWorkspaceSupportContext, RemoteNameContext, VirtualWorkspaceContext, WorkbenchStateContext, WorkspaceFolderCountContext, PanelPositionContext, TemporaryWorkspaceContext, TitleBarVisibleContext, TitleBarStyleContext, IsAuxiliaryWindowFocusedContext, ActiveEditorGroupEmptyContext, ActiveEditorGroupIndexContext, ActiveEditorGroupLastContext, ActiveEditorGroupLockedContext, MultipleEditorGroupsContext, EditorsVisibleContext } from 'vs/workbench/common/contextkeys'; import { trackFocus, addDisposableListener, EventType, onDidRegisterWindow, getActiveWindow } from 'vs/base/browser/dom'; import { preferredSideBySideGroupDirection, GroupDirection, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -24,12 +24,21 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { getTitleBarStyle } from 'vs/platform/window/common/window'; import { mainWindow } from 'vs/base/browser/window'; import { isFullscreen, onDidChangeFullscreen } from 'vs/base/browser/browser'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; export class WorkbenchContextKeysHandler extends Disposable { private inputFocusedContext: IContextKey; private dirtyWorkingCopiesContext: IContextKey; + private activeEditorGroupEmpty: IContextKey; + private activeEditorGroupIndex: IContextKey; + private activeEditorGroupLast: IContextKey; + private activeEditorGroupLocked: IContextKey; + private multipleEditorGroupsContext: IContextKey; + + private editorsVisibleContext: IContextKey; + private splitEditorsVerticallyContext: IContextKey; private workbenchStateContext: IContextKey; @@ -64,6 +73,7 @@ export class WorkbenchContextKeysHandler extends Disposable { @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IProductService private readonly productService: IProductService, @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, + @IEditorService private readonly editorService: IEditorService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService, @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, @@ -98,6 +108,16 @@ export class WorkbenchContextKeysHandler extends Disposable { ProductQualityContext.bindTo(this.contextKeyService).set(this.productService.quality || ''); EmbedderIdentifierContext.bindTo(this.contextKeyService).set(productService.embedderIdentifier); + // Editor Groups + this.activeEditorGroupEmpty = ActiveEditorGroupEmptyContext.bindTo(this.contextKeyService); + this.activeEditorGroupIndex = ActiveEditorGroupIndexContext.bindTo(this.contextKeyService); + this.activeEditorGroupLast = ActiveEditorGroupLastContext.bindTo(this.contextKeyService); + this.activeEditorGroupLocked = ActiveEditorGroupLockedContext.bindTo(this.contextKeyService); + this.multipleEditorGroupsContext = MultipleEditorGroupsContext.bindTo(this.contextKeyService); + + // Editors + this.editorsVisibleContext = EditorsVisibleContext.bindTo(this.contextKeyService); + // Working Copies this.dirtyWorkingCopiesContext = DirtyWorkingCopiesContext.bindTo(this.contextKeyService); this.dirtyWorkingCopiesContext.set(this.workingCopyService.hasDirty); @@ -183,8 +203,18 @@ export class WorkbenchContextKeysHandler extends Disposable { private registerListeners(): void { this.editorGroupService.whenReady.then(() => { this.updateEditorAreaContextKeys(); + this.updateEditorGroupContextKeys(); + this.updateVisiblePanesContextKeys(); }); + this._register(this.editorService.onDidActiveEditorChange(() => this.updateEditorGroupContextKeys())); + this._register(this.editorService.onDidVisibleEditorsChange(() => this.updateVisiblePanesContextKeys())); + this._register(this.editorGroupService.onDidAddGroup(() => this.updateEditorGroupContextKeys())); + this._register(this.editorGroupService.onDidRemoveGroup(() => this.updateEditorGroupContextKeys())); + this._register(this.editorGroupService.onDidChangeGroupIndex(() => this.updateEditorGroupContextKeys())); + this._register(this.editorGroupService.onDidChangeActiveGroup(() => this.updateEditorGroupsContextKeys())); + this._register(this.editorGroupService.onDidChangeGroupLocked(() => this.updateEditorGroupsContextKeys())); + this._register(this.editorGroupService.onDidChangeEditorPartOptions(() => this.updateEditorAreaContextKeys())); this._register(Event.runAndSubscribe(onDidRegisterWindow, ({ window, disposables }) => disposables.add(addDisposableListener(window, EventType.FOCUS_IN, () => this.updateInputContextKeys(window.document), true)), { window: mainWindow, disposables: this._store })); @@ -227,6 +257,38 @@ export class WorkbenchContextKeysHandler extends Disposable { this._register(this.workingCopyService.onDidChangeDirty(workingCopy => this.dirtyWorkingCopiesContext.set(workingCopy.isDirty() || this.workingCopyService.hasDirty))); } + private updateVisiblePanesContextKeys(): void { + const visibleEditorPanes = this.editorService.visibleEditorPanes; + if (visibleEditorPanes.length > 0) { + this.editorsVisibleContext.set(true); + } else { + this.editorsVisibleContext.reset(); + } + } + + private updateEditorGroupContextKeys(): void { + if (!this.editorService.activeEditor) { + this.activeEditorGroupEmpty.set(true); + } else { + this.activeEditorGroupEmpty.reset(); + } + this.updateEditorGroupsContextKeys(); + } + + private updateEditorGroupsContextKeys(): void { + const groupCount = this.editorGroupService.count; + if (groupCount > 1) { + this.multipleEditorGroupsContext.set(true); + } else { + this.multipleEditorGroupsContext.reset(); + } + + const activeGroup = this.editorGroupService.activeGroup; + this.activeEditorGroupIndex.set(activeGroup.index + 1); // not zero-indexed + this.activeEditorGroupLast.set(activeGroup.index === groupCount - 1); + this.activeEditorGroupLocked.set(activeGroup.isLocked); + } + private updateEditorAreaContextKeys(): void { this.editorTabsVisibleContext.set(this.editorGroupService.partOptions.showTabs === 'multiple'); } diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index e4408c37b17..d88cd673186 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -269,7 +269,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { const groupActiveEditorAvailableEditorIds = this.editorPartsView.bind(ActiveEditorAvailableEditorIdsContext, this); const groupActiveEditorCanSplitInGroupContext = this.editorPartsView.bind(ActiveEditorCanSplitInGroupContext, this); - const sideBySideEditorContext = this.editorPartsView.bind(SideBySideEditorActiveContext, this); + const groupActiveEditorIsSideBySideEditorContext = this.editorPartsView.bind(SideBySideEditorActiveContext, this); const activeEditorListener = this._register(new MutableDisposable()); @@ -286,7 +286,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { if (activeEditor) { groupActiveEditorCanSplitInGroupContext.set(activeEditor.hasCapability(EditorInputCapabilities.CanSplitInGroup)); - sideBySideEditorContext.set(activeEditor.typeId === SideBySideEditorInput.ID); + groupActiveEditorIsSideBySideEditorContext.set(activeEditor.typeId === SideBySideEditorInput.ID); groupActiveEditorDirtyContext.set(activeEditor.isDirty() && !activeEditor.isSaving()); activeEditorListener.value = activeEditor.onDidChangeDirty(() => { @@ -294,7 +294,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { }); } else { groupActiveEditorCanSplitInGroupContext.set(false); - sideBySideEditorContext.set(false); + groupActiveEditorIsSideBySideEditorContext.set(false); groupActiveEditorDirtyContext.set(false); } From fd7c7bda0f55604dee7b7d0629be3f5ea6080fa4 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 23 May 2024 13:16:18 -0700 Subject: [PATCH 355/357] Render html as plaintext when html not supported (#213265) * Support rendering unsupported html tags as plaintext for chat * Render html as plaintext when html not supported * Add comment and test for trusted domains --- src/vs/base/browser/markdownRenderer.ts | 63 ++++++++++-- .../contrib/chat/browser/chatListRenderer.ts | 14 ++- .../chat/browser/chatMarkdownRenderer.ts | 79 +++++++++++++++ .../ChatMarkdownRenderer_CDATA.0.snap | 1 + .../ChatMarkdownRenderer_html_comments.0.snap | 1 + .../ChatMarkdownRenderer_invalid_HTML.0.snap | 1 + ...nderer_invalid_HTML_with_attributes.0.snap | 1 + ...nderer_mixed_valid_and_invalid_HTML.0.snap | 8 ++ .../ChatMarkdownRenderer_remote_images.0.snap | 1 + ...kdownRenderer_self-closing_elements.0.snap | 1 + .../ChatMarkdownRenderer_simple.0.snap | 1 + .../ChatMarkdownRenderer_valid_HTML.0.snap | 6 ++ .../test/browser/chatMarkdownRenderer.test.ts | 99 +++++++++++++++++++ .../test/browser/mockTrustedDomainService.ts | 18 ++++ 14 files changed, 280 insertions(+), 14 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/chatMarkdownRenderer.ts create mode 100644 src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_CDATA.0.snap create mode 100644 src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_html_comments.0.snap create mode 100644 src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_invalid_HTML.0.snap create mode 100644 src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_invalid_HTML_with_attributes.0.snap create mode 100644 src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_mixed_valid_and_invalid_HTML.0.snap create mode 100644 src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_remote_images.0.snap create mode 100644 src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_self-closing_elements.0.snap create mode 100644 src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_simple.0.snap create mode 100644 src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_valid_HTML.0.snap create mode 100644 src/vs/workbench/contrib/chat/test/browser/chatMarkdownRenderer.test.ts create mode 100644 src/vs/workbench/contrib/url/test/browser/mockTrustedDomainService.ts diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts index 5a7f7fe9aa5..b1a304845a3 100644 --- a/src/vs/base/browser/markdownRenderer.ts +++ b/src/vs/base/browser/markdownRenderer.ts @@ -36,6 +36,12 @@ export interface MarkdownRenderOptions extends FormattedTextRenderOptions { readonly asyncRenderCallback?: () => void; readonly fillInIncompleteTokens?: boolean; readonly remoteImageIsAllowed?: (uri: URI) => boolean; + readonly sanitizerOptions?: ISanitizerOptions; +} + +export interface ISanitizerOptions { + replaceWithPlaintext?: boolean; + allowedTags?: string[]; } const defaultMarkedRenderers = Object.freeze({ @@ -221,6 +227,10 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende // We always pass the output through dompurify after this so that we don't rely on // marked for sanitization. markedOptions.sanitizer = (html: string): string => { + if (options.sanitizerOptions?.replaceWithPlaintext) { + return escape(html); + } + const match = markdown.isTrusted ? html.match(/^(]+>)|(<\/\s*span>)$/) : undefined; return match ? html : ''; }; @@ -261,7 +271,7 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende } const htmlParser = new DOMParser(); - const markdownHtmlDoc = htmlParser.parseFromString(sanitizeRenderedMarkdown(markdown, renderedMarkdown) as unknown as string, 'text/html'); + const markdownHtmlDoc = htmlParser.parseFromString(sanitizeRenderedMarkdown({ isTrusted: markdown.isTrusted, ...options.sanitizerOptions }, renderedMarkdown) as unknown as string, 'text/html'); markdownHtmlDoc.body.querySelectorAll('img, audio, video, source') .forEach(img => { @@ -306,7 +316,7 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende } }); - element.innerHTML = sanitizeRenderedMarkdown(markdown, markdownHtmlDoc.body.innerHTML) as unknown as string; + element.innerHTML = sanitizeRenderedMarkdown({ isTrusted: markdown.isTrusted, ...options.sanitizerOptions }, markdownHtmlDoc.body.innerHTML) as unknown as string; if (codeBlocks.length > 0) { Promise.all(codeBlocks).then((tuples) => { @@ -378,8 +388,14 @@ function resolveWithBaseUri(baseUri: URI, href: string): string { } } +interface IInternalSanitizerOptions extends ISanitizerOptions { + isTrusted?: boolean | MarkdownStringTrustedOptions; +} + +const selfClosingTags = ['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr']; + function sanitizeRenderedMarkdown( - options: { isTrusted?: boolean | MarkdownStringTrustedOptions }, + options: IInternalSanitizerOptions, renderedMarkdown: string, ): TrustedHTML { const { config, allowedSchemes } = getSanitizerOptions(options); @@ -410,10 +426,45 @@ function sanitizeRenderedMarkdown( if (e.tagName === 'input') { if (element.attributes.getNamedItem('type')?.value === 'checkbox') { element.setAttribute('disabled', ''); - } else { + } else if (!options.replaceWithPlaintext) { element.parentElement?.removeChild(element); } } + + if (options.replaceWithPlaintext && !e.allowedTags[e.tagName] && e.tagName !== 'body') { + if (element.parentElement) { + let startTagText: string; + let endTagText: string | undefined; + if (e.tagName === '#comment') { + startTagText = ``; + } else { + const isSelfClosing = selfClosingTags.includes(e.tagName); + const attrString = element.attributes.length ? + ' ' + Array.from(element.attributes) + .map(attr => `${attr.name}="${attr.value}"`) + .join(' ') + : ''; + startTagText = `<${e.tagName}${attrString}>`; + if (!isSelfClosing) { + endTagText = ``; + } + } + + const fragment = document.createDocumentFragment(); + const textNode = element.parentElement.ownerDocument.createTextNode(startTagText); + fragment.appendChild(textNode); + const endTagTextNode = endTagText ? element.parentElement.ownerDocument.createTextNode(endTagText) : undefined; + while (element.firstChild) { + fragment.appendChild(element.firstChild); + } + + if (endTagTextNode) { + fragment.appendChild(endTagTextNode); + } + + element.parentElement.replaceChild(fragment, element); + } + } })); store.add(DOM.hookDomPurifyHrefAndSrcSanitizer(allowedSchemes)); @@ -451,7 +502,7 @@ export const allowedMarkdownAttr = [ 'start', ]; -function getSanitizerOptions(options: { readonly isTrusted?: boolean | MarkdownStringTrustedOptions }): { config: dompurify.Config; allowedSchemes: string[] } { +function getSanitizerOptions(options: IInternalSanitizerOptions): { config: dompurify.Config; allowedSchemes: string[] } { const allowedSchemes = [ Schemas.http, Schemas.https, @@ -473,7 +524,7 @@ function getSanitizerOptions(options: { readonly isTrusted?: boolean | MarkdownS // Since we have our own sanitize function for marked, it's possible we missed some tag so let dompurify make sure. // HTML tags that can result from markdown are from reading https://spec.commonmark.org/0.29/ // HTML table tags that can result from markdown are from https://github.github.com/gfm/#tables-extension- - ALLOWED_TAGS: [...DOM.basicMarkupHtmlTags], + ALLOWED_TAGS: options.allowedTags ?? [...DOM.basicMarkupHtmlTags], ALLOWED_ATTR: allowedMarkdownAttr, ALLOW_UNKNOWN_PROTOCOLS: true, }, diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index a3417d2154f..66363a9abc6 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -4,6 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; +import { renderFormattedText } from 'vs/base/browser/formattedTextRenderer'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { alert } from 'vs/base/browser/ui/aria/aria'; import { Button } from 'vs/base/browser/ui/button/button'; @@ -21,6 +23,7 @@ import { Codicon } from 'vs/base/common/codicons'; import { Emitter, Event } from 'vs/base/common/event'; import { FuzzyScore } from 'vs/base/common/filters'; import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; +import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ResourceMap } from 'vs/base/common/map'; import { FileAccess, Schemas, matchesSomeScheme } from 'vs/base/common/network'; @@ -68,19 +71,16 @@ import { ChatAgentLocation, IChatAgentMetadata } from 'vs/workbench/contrib/chat import { CONTEXT_CHAT_RESPONSE_SUPPORT_ISSUE_REPORTING, CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_DETECTED_AGENT_COMMAND, CONTEXT_RESPONSE_FILTERED, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatProgressRenderableResponseContent, IChatTextEditGroup } from 'vs/workbench/contrib/chat/common/chatModel'; import { chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { IChatCommandButton, IChatConfirmation, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseProgressFileTreeData, IChatSendRequestOptions, IChatService, IChatTask, IChatWarningMessage, ChatAgentVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { ChatAgentVoteDirection, IChatCommandButton, IChatConfirmation, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseProgressFileTreeData, IChatSendRequestOptions, IChatService, IChatTask, IChatWarningMessage } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import { IChatProgressMessageRenderData, IChatRenderData, IChatResponseMarkdownRenderData, IChatResponseViewModel, IChatTaskRenderData, IChatWelcomeMessageViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { IWordCountResult, getNWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; import { createFileIconThemableTreeContainerScope } from 'vs/workbench/contrib/files/browser/views/explorerView'; import { IFilesConfiguration } from 'vs/workbench/contrib/files/common/files'; -import { ITrustedDomainService } from 'vs/workbench/contrib/url/browser/trustedDomainService'; import { IMarkdownVulnerability, annotateSpecialMarkdownContent } from '../common/annotations'; import { CodeBlockModelCollection } from '../common/codeBlockModelCollection'; import { IChatListItemRendererOptions } from './chat'; -import { renderFormattedText } from 'vs/base/browser/formattedTextRenderer'; -import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { KeyCode } from 'vs/base/common/keyCodes'; +import { ChatMarkdownRenderer } from 'vs/workbench/contrib/chat/browser/chatMarkdownRenderer'; const $ = dom.$; @@ -160,13 +160,12 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer this.trustedDomainService.isValid(uri), fillInIncompleteTokens, codeBlockRendererSync: (languageId, text) => { const index = codeBlockIndex++; diff --git a/src/vs/workbench/contrib/chat/browser/chatMarkdownRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatMarkdownRenderer.ts new file mode 100644 index 00000000000..225ecdd120e --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatMarkdownRenderer.ts @@ -0,0 +1,79 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { MarkdownRenderOptions, MarkedOptions } from 'vs/base/browser/markdownRenderer'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { IMarkdownRendererOptions, IMarkdownRenderResult, MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { ITrustedDomainService } from 'vs/workbench/contrib/url/browser/trustedDomainService'; + +const allowedHtmlTags = [ + 'b', + 'blockquote', + 'br', + 'code', + 'em', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'hr', + 'i', + 'li', + 'ol', + 'p', + 'pre', + 'strong', + 'table', + 'tbody', + 'td', + 'th', + 'thead', + 'tr', + 'ul', + 'a', + 'img', + + // Not in the official list, but used for codicons and other vscode markdown extensions + 'span', +]; + +/** + * This wraps the MarkdownRenderer and applies sanitizer options needed for Chat. + */ +export class ChatMarkdownRenderer extends MarkdownRenderer { + constructor( + options: IMarkdownRendererOptions | undefined, + @ILanguageService languageService: ILanguageService, + @IOpenerService openerService: IOpenerService, + @ITrustedDomainService private readonly trustedDomainService: ITrustedDomainService, + ) { + super(options ?? {}, languageService, openerService); + } + + override render(markdown: IMarkdownString | undefined, options?: MarkdownRenderOptions, markedOptions?: MarkedOptions): IMarkdownRenderResult { + options = { + ...options, + remoteImageIsAllowed: (uri) => this.trustedDomainService.isValid(uri), + sanitizerOptions: { + replaceWithPlaintext: true, + allowedTags: allowedHtmlTags, + } + }; + + const mdWithBody: IMarkdownString | undefined = (markdown && markdown.supportHtml) ? + { + ...markdown, + + // dompurify uses DOMParser, which strips leading comments. Wrapping it all in 'body' prevents this. + value: `${markdown.value}`, + } + : markdown; + return super.render(mdWithBody, options, markedOptions); + } +} diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_CDATA.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_CDATA.0.snap new file mode 100644 index 00000000000..67f63f14b70 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_CDATA.0.snap @@ -0,0 +1 @@ +
<!--[CDATA[<div-->content]]>
\ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_html_comments.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_html_comments.0.snap new file mode 100644 index 00000000000..c1ba30be800 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_html_comments.0.snap @@ -0,0 +1 @@ +
<!-- comment1 <div></div> --><div>content</div><!-- comment2 -->
\ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_invalid_HTML.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_invalid_HTML.0.snap new file mode 100644 index 00000000000..02c52ac2aa4 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_invalid_HTML.0.snap @@ -0,0 +1 @@ +
1<canvas>2<div>3</div></canvas>4
\ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_invalid_HTML_with_attributes.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_invalid_HTML_with_attributes.0.snap new file mode 100644 index 00000000000..67381fee546 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_invalid_HTML_with_attributes.0.snap @@ -0,0 +1 @@ +
1<div id="id1" style="display: none">2<div id="my id 2">3</div></div>4
\ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_mixed_valid_and_invalid_HTML.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_mixed_valid_and_invalid_HTML.0.snap new file mode 100644 index 00000000000..a58ce687e96 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_mixed_valid_and_invalid_HTML.0.snap @@ -0,0 +1,8 @@ +

heading

+<div> +
    +
  • <div>1</div>
  • +
  • hi
  • +
+</div> +
<canvas>canvas here</canvas>
<details></details>
\ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_remote_images.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_remote_images.0.snap new file mode 100644 index 00000000000..247cce5ff8e --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_remote_images.0.snap @@ -0,0 +1 @@ +
<div><img src="http://disallowed.com/image.jpg"></div>
\ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_self-closing_elements.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_self-closing_elements.0.snap new file mode 100644 index 00000000000..023b2e6a846 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_self-closing_elements.0.snap @@ -0,0 +1 @@ +
<area>

<input type="text" value="test">
\ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_simple.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_simple.0.snap new file mode 100644 index 00000000000..2e65efe2a14 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_simple.0.snap @@ -0,0 +1 @@ +a \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_valid_HTML.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_valid_HTML.0.snap new file mode 100644 index 00000000000..df6a95f4b5d --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_valid_HTML.0.snap @@ -0,0 +1,6 @@ +

heading

+
    +
  • 1
  • +
  • hi
  • +
+
code here
\ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/chatMarkdownRenderer.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatMarkdownRenderer.test.ts new file mode 100644 index 00000000000..f006d1afbf7 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/chatMarkdownRenderer.test.ts @@ -0,0 +1,99 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { MarkdownString } from 'vs/base/common/htmlContent'; +import { assertSnapshot } from 'vs/base/test/common/snapshot'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { ChatMarkdownRenderer } from 'vs/workbench/contrib/chat/browser/chatMarkdownRenderer'; +import { ITrustedDomainService } from 'vs/workbench/contrib/url/browser/trustedDomainService'; +import { MockTrustedDomainService } from 'vs/workbench/contrib/url/test/browser/mockTrustedDomainService'; +import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; + +suite('ChatMarkdownRenderer', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + let testRenderer: ChatMarkdownRenderer; + setup(() => { + const instantiationService = store.add(workbenchInstantiationService(undefined, store)); + instantiationService.stub(ITrustedDomainService, new MockTrustedDomainService(['http://allowed.com'])); + testRenderer = instantiationService.createInstance(ChatMarkdownRenderer, {}); + }); + + test('simple', async () => { + const md = new MarkdownString('a'); + const result = store.add(testRenderer.render(md)); + await assertSnapshot(result.element.textContent); + }); + + test('invalid HTML', async () => { + const md = new MarkdownString('12
3
4'); + md.supportHtml = true; + const result = store.add(testRenderer.render(md)); + await assertSnapshot(result.element.outerHTML); + }); + + test('invalid HTML with attributes', async () => { + const md = new MarkdownString('14'); + md.supportHtml = true; + const result = store.add(testRenderer.render(md)); + await assertSnapshot(result.element.outerHTML); + }); + + test('valid HTML', async () => { + const md = new MarkdownString(` +

heading

+
    +
  • 1
  • +
  • hi
  • +
+
code here
`); + md.supportHtml = true; + const result = store.add(testRenderer.render(md)); + await assertSnapshot(result.element.outerHTML); + }); + + test('mixed valid and invalid HTML', async () => { + const md = new MarkdownString(` +

heading

+
+
    +
  • 1
  • +
  • hi
  • +
+
+
canvas here
`); + md.supportHtml = true; + const result = store.add(testRenderer.render(md)); + await assertSnapshot(result.element.outerHTML); + }); + + test('self-closing elements', async () => { + const md = new MarkdownString('

'); + md.supportHtml = true; + const result = store.add(testRenderer.render(md)); + await assertSnapshot(result.element.outerHTML); + }); + + test('html comments', async () => { + const md = new MarkdownString('
content
'); + md.supportHtml = true; + const result = store.add(testRenderer.render(md)); + await assertSnapshot(result.element.outerHTML); + }); + + test('CDATA', async () => { + const md = new MarkdownString('content]]>'); + md.supportHtml = true; + const result = store.add(testRenderer.render(md)); + await assertSnapshot(result.element.outerHTML); + }); + + test('remote images', async () => { + const md = new MarkdownString(' '); + md.supportHtml = true; + const result = store.add(testRenderer.render(md)); + await assertSnapshot(result.element.outerHTML); + }); +}); diff --git a/src/vs/workbench/contrib/url/test/browser/mockTrustedDomainService.ts b/src/vs/workbench/contrib/url/test/browser/mockTrustedDomainService.ts new file mode 100644 index 00000000000..61c252892ae --- /dev/null +++ b/src/vs/workbench/contrib/url/test/browser/mockTrustedDomainService.ts @@ -0,0 +1,18 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from 'vs/base/common/uri'; +import { isURLDomainTrusted, ITrustedDomainService } from 'vs/workbench/contrib/url/browser/trustedDomainService'; + +export class MockTrustedDomainService implements ITrustedDomainService { + _serviceBrand: undefined; + + constructor(private readonly _trustedDomains: string[] = []) { + } + + isValid(resource: URI): boolean { + return isURLDomainTrusted(resource, this._trustedDomains); + } +} From 38a6ee6d39029122cd73bb04cc0c77f47303c616 Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Thu, 23 May 2024 13:49:13 -0700 Subject: [PATCH 356/357] code action and lightbulb telemetry updates (#212929) * added additional telemetry and remove unused telemetry event * removed unused imports --- .../browser/codeActionController.ts | 3 +++ .../codeAction/browser/lightBulbWidget.ts | 25 ++----------------- 2 files changed, 5 insertions(+), 23 deletions(-) diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionController.ts b/src/vs/editor/contrib/codeAction/browser/codeActionController.ts index 2d7972c19ee..65dc839da2c 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionController.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeActionController.ts @@ -317,11 +317,13 @@ export class CodeActionController extends Disposable implements IEditorContribut type ShowCodeActionListEvent = { codeActionListLength: number; didCancel: boolean; + codeActions: string[]; }; type ShowListEventClassification = { codeActionListLength: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The length of the code action list when quit out. Can be from any code action menu.' }; didCancel: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the code action was cancelled or selected.' }; + codeActions: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'What code actions were available when cancelled.' }; owner: 'justschen'; comment: 'Event used to gain insights into how many valid code actions are being shown'; }; @@ -329,6 +331,7 @@ export class CodeActionController extends Disposable implements IEditorContribut this._telemetryService.publicLog2('codeAction.showCodeActionList.onHide', { codeActionListLength: actions.validActions.length, didCancel: didCancel, + codeActions: actions.validActions.map(action => action.action.title), }); } }, diff --git a/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.ts b/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.ts index 3e62c4c0281..94518efd415 100644 --- a/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.ts +++ b/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.ts @@ -7,7 +7,6 @@ import * as dom from 'vs/base/browser/dom'; import { Gesture } from 'vs/base/browser/touch'; import { Codicon } from 'vs/base/common/codicons'; import { Emitter, Event } from 'vs/base/common/event'; -import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; import { Disposable } from 'vs/base/common/lifecycle'; import { ThemeIcon } from 'vs/base/common/themables'; import 'vs/css!./lightBulbWidget'; @@ -16,11 +15,10 @@ import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IPosition } from 'vs/editor/common/core/position'; import { computeIndentLevel } from 'vs/editor/common/model/utils'; import { autoFixCommandId, quickFixCommandId } from 'vs/editor/contrib/codeAction/browser/codeAction'; -import { CodeActionKind, CodeActionSet, CodeActionTrigger } from 'vs/editor/contrib/codeAction/common/types'; +import { CodeActionSet, CodeActionTrigger } from 'vs/editor/contrib/codeAction/common/types'; import * as nls from 'vs/nls'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; namespace LightBulbState { @@ -65,8 +63,7 @@ export class LightBulbWidget extends Disposable implements IContentWidget { constructor( private readonly _editor: ICodeEditor, @IKeybindingService private readonly _keybindingService: IKeybindingService, - @ICommandService commandService: ICommandService, - @ITelemetryService private readonly _telemetryService: ITelemetryService, + @ICommandService commandService: ICommandService ) { super(); @@ -200,24 +197,6 @@ export class LightBulbWidget extends Disposable implements IContentWidget { return; } - const hierarchicalKind = new HierarchicalKind(actionKind); - - if (CodeActionKind.RefactorMove.contains(hierarchicalKind)) { - // Telemetry for showing code actions here. only log on `showLightbulb`. Logs when code action list is quit out. - type ShowCodeActionListEvent = { - codeActionListLength: number; - }; - - type ShowListEventClassification = { - codeActionListLength: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The length of the code action list when quit out. Can be from any code action menu.' }; - owner: 'justschen'; - comment: 'Event used to gain insights into how often the lightbulb only contains one code action, namely the move to code action. '; - }; - - this._telemetryService.publicLog2('lightbulbWidget.moveToCodeActions', { - codeActionListLength: validActions.length, - }); - } this._editor.layoutContentWidget(this); } From a0986b0f653c8309e8a957d53a0a5039239c6a5d Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 23 May 2024 14:49:39 -0700 Subject: [PATCH 357/357] Bump distro (#213340) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a7a658eab13..6bd79ca053e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.90.0", - "distro": "91d9dc4bb762ea314baeaf02cd6ede0d8148b04e", + "distro": "0f8d3c619e9a416854972b1f496b1eedc727ab18", "author": { "name": "Microsoft Corporation" },